:root {
  --kb-brand-primary: #1f2937;
  --kb-brand-accent: #e5e7eb;
  --kb-bg: #ffffff;
  --kb-fg: #111212;
  --kb-muted: #6b7280;
  --kb-border: #e5e7eb;
  /* The frame: how wide the page's content is allowed to get, and what the
     header, the footer and the tree pane are centred within. */
  --kb-max-width: 78rem;
  /* The reading measure inside the article pane. Narrower than the frame
     minus the tree, because the pane now runs to the edge of the window. */
  --kb-doc-width: 60rem;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--kb-bg);
  color: var(--kb-fg);
  font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* AN APP SHELL, NOT A SCROLLING DOCUMENT, which is how the app's own
     community pages work (CommunityPagesShell: `flex h-full min-h-0`, with
     `overflow-y-auto` on each pane). The window holds the header, the panes
     and the footer; whatever overflows scrolls INSIDE the pane it belongs to.

     That is what makes the tree and the article scroll independently -- a
     reader deep in a long article does not have to scroll it back to reach
     the tree -- and it is why the footer needs no sticky trickery to stay at
     the bottom of the window: it is the last item in a column exactly one
     window tall, so it is always there.

     `dvh` so a mobile browser's collapsing toolbar does not leave a gap;
     `vh` first for anything that does not know the unit. */
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
.kb-header, .kb-footer { flex: none; }
/* The scroll root for the pages that have no tree -- the org home and the org
   search. A page WITH a tree never scrolls here: the shell below is bounded by
   this element and its two panes are each their own scroller, so there is
   nothing left over and no second scrollbar appears. */
.kb-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}
a { color: inherit; }

.kb-header {
  background: var(--kb-brand-primary);
  border-top: 4px solid var(--kb-brand-accent);
  color: #fff;
  padding: 1.25rem 1.5rem;
  position: relative;
}
.kb-header__hero {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  opacity: .35;
}
.kb-header__inner {
  position: relative;
  max-width: var(--kb-max-width);
  margin: 0 auto;
  /* Brand on the left, search on the right, stacking on a narrow screen. */
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem 1.5rem;
  flex-wrap: wrap;
}
.kb-header__brand { min-width: 0; }
.kb-header__logo { height: 2.25rem; display: block; margin-bottom: .5rem; }
.kb-header__title { font-size: 1.4rem; font-weight: 650; margin: 0; }

/* `Home / <community>` under the title, where the classic KB drew it.
   Muted against the banner rather than plain white, so it reads as a location
   and not as a second heading. Absent on the org front door and on a custom
   domain, where nothing sits above this artifact -- see Layout.astro. */
.kb-breadcrumb { margin-top: .35rem; font-size: .82rem; }
.kb-breadcrumb__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.kb-breadcrumb__item {
  display: flex;
  align-items: center;
  gap: .35rem;
  color: rgba(255, 255, 255, .78);
}
/* Generated, not marked up: the separator is punctuation BETWEEN crumbs, not a
   crumb, so a screen reader reading the list must not count it as one. */
.kb-breadcrumb__item + .kb-breadcrumb__item::before {
  content: '/';
  color: rgba(255, 255, 255, .45);
}
.kb-breadcrumb__item a { text-decoration: none; }
.kb-breadcrumb__item a:hover { color: #fff; text-decoration: underline; }

/* The crumb the reader is already on, brightened so the muted ones beside it
   read as the links they are. Without this, `Home` and `Search` paint the same
   -- `a { color: inherit }` above, and these crumbs carry no underline -- and
   the way out of the page is indistinguishable from the label of the page.

   This does NOT reopen the decision recorded in Layout.astro's `IDENTICAL ON
   EVERY PAGE OF A COMMUNITY` comment. That rejects marking the trailing crumb
   on a COMMUNITY page, where it would make the header vary within one
   community and so drag it into pjax's sync contract. The derived arm emits no
   `aria-current` at all, so this selector matches the org-wide search page
   alone -- and pjax returns early on org pages. */
.kb-breadcrumb__item [aria-current='page'] { color: #fff; }

/* The hero lays its inner out as a centred column and centres the title, so a
   crumb left at the default `flex-start` sits hard left under a centred
   heading. Only the org-wide search page draws one there -- see Layout's
   `crumbs` prop; the front door deliberately has none. */
.kb-header--hero .kb-breadcrumb__list { justify-content: center; }

/* The header search box.
   Coloured in translucent white rather than from the palette, because this
   sits on `--kb-brand-primary` -- a customer-chosen colour this header
   already assumes is dark enough for white text. Following that assumption
   keeps the box legible on every brand the header itself works on; picking a
   palette colour would be a second, different assumption. */
.kb-header__search { display: flex; gap: .375rem; flex: 0 1 22rem; min-width: 12rem; }
.kb-header__search-input {
  flex: 1;
  min-width: 0;
  padding: .4rem .7rem;
  font: inherit;
  font-size: .9rem;
  color: #fff;
  background: rgba(255, 255, 255, .14);
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: .375rem;
}
.kb-header__search-input::placeholder { color: rgba(255, 255, 255, .72); }
.kb-header__search-input:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 1px;
  background: rgba(255, 255, 255, .22);
}
/* Chrome and Safari draw their own clear button on a type=search input, in a
   colour chosen for a light field. */
.kb-header__search-input::-webkit-search-cancel-button { filter: invert(1); opacity: .7; }
.kb-header__search-button {
  flex: none;
  padding: .4rem .8rem;
  font: inherit;
  font-size: .9rem;
  color: #fff;
  background: rgba(255, 255, 255, .18);
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: .375rem;
  cursor: pointer;
}
.kb-header__search-button:hover { background: rgba(255, 255, 255, .28); }
.kb-header__search-button:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }

/* THE ORG PAGES' BANNER, which the classic KB laid out differently from a
   community's: the title centred over a wide search box rather than a brand on
   the left and a compact box on the right. Same markup, different rules --
   Layout.astro only adds the modifier class.

   The logo stays LEFT while the title centres, which is not an accident of the
   classic's markup but the shape of it: `.logo` was its own full-width row at
   `justify-content-start`, with the title centred in the row below. So the
   brand block goes full width here and only the title is centred. */
.kb-header--hero { padding: 3rem 1.5rem; }
.kb-header--hero .kb-header__inner {
  flex-direction: column;
  align-items: stretch;
  gap: 1.25rem;
}
.kb-header--hero .kb-header__title { font-size: 1.9rem; text-align: center; }
.kb-header--hero .kb-header__search {
  /* 560px is the classic's own width for this box. `min()` so it still fits a
     phone, where `flex: none` would otherwise overflow the banner. */
  align-self: center;
  flex: none;
  width: min(560px, 100%);
}

/* Two panes and a divider.
   `gap` is 0 because the divider IS the gap: it wants a hit area wider than
   the hairline it draws, and a gap either side of that would put dead space
   where the reader aims.

   `min-height: 0` is what lets the two panes overflow into their own
   scrollers instead of pushing the shell past the window. A flex item's
   default `min-height: auto` refuses to shrink below its content, which is
   exactly how an app-shell layout ends up with a document scrollbar anyway.

   FRAMED ON THE LEFT, FLUSH ON THE RIGHT, which is the shape both reports
   asked for and neither `margin: 0 auto` nor full bleed can give on its own.

   Capped and centred, the article pane stopped short of the right edge of the
   screen and took its scrollbar with it, leaving the bar against the prose
   with a band of background beyond it. Full bleed put the bar on the edge but
   let the tree and the article run the whole width of a wide monitor, which
   is a worse thing to read.

   So the frame is a left inset -- exactly the margin `margin: 0 auto` would
   have produced, which is what lines the tree pane up with the header and the
   footer above and below it -- while the right side stays flush, so the
   article pane reaches the window and its scrollbar rides the edge. What
   stops the prose stretching with it is --kb-doc-width, on the pane's
   contents rather than on the pane. Classic external KBs land in the same
   place by a different route: a centred container, with the scrollbar on the
   edge because it is the window's. */
.kb-shell {
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
  gap: 0;
  padding: 0;
  padding-left: max(0px, (100vw - var(--kb-max-width)) / 2);
}
.kb-nav {
  /* Set by scripts/resizer.js on <html>; this is the width before it runs and
     the width when it cannot (no JS). */
  width: var(--kb-nav-width, 260px);
  flex: none;
  min-width: 0;
  /* Left only, so the rows are not flush against the window. It stays out
     here rather than going in the scroller because the scroller's padding is
     also inset from the rows' hover band, and a band that starts short of
     the pane's edge reads as a misaligned row.
     NO RIGHT PADDING here. It was `padding-right: 1rem`, which is outside the
     scroller below, and that put the pane's whole scrollbar 16px clear of the
     divider -- floating in the middle of the pane rather than at its edge --
     while the rows, which end at the scroller's own edge, ran underneath it.
     A macOS overlay scrollbar is painted over the content, so it takes no
     layout space and nothing stops text from passing beneath it. The padding
     belongs INSIDE the scroller: then the scrollbar sits at the pane's edge,
     next to the divider, and the padding is the gap it is drawn in. */
  padding-left: .75rem;
  /* A column so the tree inside can be the part that scrolls, which is what
     leaves room to put a fixed heading above it later without the heading
     scrolling away -- the app's DocTree is built the same way. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
/* The tree's own scroller. This, not the pane and not the window, is what a
   tree taller than the screen scrolls in. */
.kb-nav__tree {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Top and bottom match .kb-main's, so the first row of the tree starts on
     the same line as the first line of the page rather than a third of an
     inch above it. Inside the scroller, as the article's is, so the two also
     behave alike once the reader scrolls: the gap goes with the content.
     The right side is the scrollbar's gutter -- see .kb-nav above. */
  padding: 1.5rem .75rem 1.5rem 0;
  /* Where scrollbars take real space (Windows, and macOS set to show them
     always) the gutter is reserved whether or not one is showing, so
     expanding a folder past the bottom of the pane cannot reflow every row
     and move all the truncation with it. */
  scrollbar-gutter: stable;
}
/* The reading measure. It is on the pane's CONTENTS, not the pane, because
   the pane has to keep reaching the window for its scrollbar to sit on the
   edge -- so the column of prose stops where the frame would have, and the
   gap beyond it is what the centring margin would have been.
   A shell with no tree (the org home, the org search) has nothing to leave
   room for, so it fills the frame instead. */
[data-kb-shell] .kb-main > * { max-width: var(--kb-doc-width); }
.kb-shell--wide .kb-main > * { max-width: var(--kb-max-width); margin-inline: auto; }
/* AND NO LEFT INSET ON THE ORG PAGES. The shell's `padding-left` exists to
   line a community's TREE PANE up with the header while the article pane stays
   flush right, so its scrollbar rides the window's edge. An org page has no
   tree, so the inset buys nothing there and costs symmetry: it pushed the
   whole front door right of centre by half the frame's margin, which reads as
   a mistake next to a centred hero. Dropping it lets `margin-inline: auto`
   above centre the content against the window, as the classic's `.container`
   did, with the scrollbar still on the edge because `.kb-main` still spans. */
.kb-shell--wide { padding-left: 0; }

/* The page's own scroller, matching the app's right pane
   (`min-h-0 min-w-0 flex-1 overflow-y-auto`). Both `min-*: 0` are load
   bearing: without the height one the article pushes the shell past the
   window and the scrollbar becomes the window's again, and without the width
   one a long unbroken line in the prose widens the pane instead of scrolling
   inside it. */
.kb-main {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  /* Inside the scroller, so the article's scrollbar rides the right edge of
     the window and this is the gap it is drawn in. */
  padding: 1.5rem;
  scrollbar-gutter: stable;
}

/* The divider. 11px of grab area, one pixel of ink. */
.kb-resizer {
  flex: none;
  align-self: stretch;
  position: relative;
  width: 11px;
  /* Pulled left so the INK lands exactly on the tree pane's right edge, with
     the grab area straddling it -- which is the app's geometry, where the
     line is the pane's own `border-r` and the handle is a transparent strip
     at `right-[-3px]`. It used to sit 5px inside this strip and therefore 5px
     clear of the pane, which is the gap between the tree's scrollbar and the
     divider that made the bar look adrift. */
  margin: 0 0 0 -5px;
  cursor: col-resize;
  /* Stops a touch drag scrolling the page instead of moving the divider. */
  touch-action: none;
}
.kb-resizer::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 5px;
  width: 1px;
  background: var(--kb-border);
  transition: background-color .12s ease, width .12s ease;
}
.kb-resizer:hover::before,
.kb-resizer:focus-visible::before,
.kb-resizer[data-dragging]::before {
  width: 2px;
  background: var(--kb-brand-primary);
}
.kb-resizer:focus-visible { outline: none; }
/* While dragging, the pointer is outside the 11px handle almost immediately.
   Without this the drag selects the article's text and the cursor flickers
   between a caret and the resize arrows. */
[data-kb-resizing] { cursor: col-resize; user-select: none; }
[data-kb-resizing] .kb-resizer::before { width: 2px; background: var(--kb-brand-primary); }

.kb-crumbs { color: var(--kb-muted); font-size: .82rem; margin-bottom: .5rem; }
.kb-crumbs a { text-decoration: none; }
.kb-title { font-size: 1.9rem; margin: 0 0 .25rem; line-height: 1.2; }
.kb-updated { color: var(--kb-muted); font-size: .82rem; margin-bottom: 1.25rem; }

/* WHAT A CONTAINER PAGE LISTS: the folders inside it as cards, then the pages
   as rows. Rendered by components/Browse.astro on both surfaces that have
   children to show -- a community's index and a folder's own page.

   This replaced `.kb-cards` / `.kb-card`, a flat row of small cards carrying
   nothing but a title. Those classes are gone rather than kept alongside:
   after the org front door moved to `.kb-category` they had exactly these two
   users, and a card style with no user is dead weight in a bundle every page
   of the site loads. */
.kb-folders {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.kb-folder {
  /* Title at the top, meta at the bottom, whatever the title's length -- which
     is what `space-between` plus a floor does and a plain stack does not. In a
     grid row the cards are already equal height, so the floor is only there to
     keep the proportions when every title is one line. */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1.25rem;
  min-height: 7rem;
  height: 100%;
  padding: 1rem;
  border: 1px solid var(--kb-border);
  border-radius: .5rem;
  background: var(--kb-bg);
  color: inherit;
  text-decoration: none;
}
.kb-folder:hover { border-color: var(--kb-brand-primary); }
.kb-folder:focus-visible { outline: 2px solid var(--kb-brand-primary); outline-offset: 2px; }
.kb-folder__head { display: flex; align-items: center; gap: .625rem; min-width: 0; }
/* A tinted tile, derived from the tenant's own colour rather than a fixed
   indigo: this is a white-label site and `--kb-brand-primary` is customer
   chosen. The flat neutral first is the fallback a browser without color-mix
   keeps, so the tile is never an invisible white square on white. */
.kb-folder__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: .5rem;
  background: var(--kb-border);
  background: color-mix(in srgb, var(--kb-brand-primary) 12%, #fff);
  color: var(--kb-brand-primary);
}
.kb-folder__title {
  min-width: 0;
  font-weight: 600;
  font-size: .95rem;
  line-height: 1.35;
}
.kb-folder__meta { color: var(--kb-muted); font-size: .8rem; }

.kb-browse__label {
  margin: 2rem 0 0;
  color: var(--kb-muted);
  font-size: .7rem;
  font-weight: 600;
  /* Uppercased here rather than in the markup, so a screen reader announces a
     word instead of spelling out an initialism. */
  text-transform: uppercase;
  letter-spacing: .07em;
}
/* One hairline above the list and one below every row, which is the n+1 rules
   the design draws -- `border-bottom` alone would leave the first row's top
   edge open against the label. */
.kb-rows {
  margin: .5rem 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--kb-border);
}
.kb-row {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .75rem .25rem;
  border-bottom: 1px solid var(--kb-border);
  color: inherit;
  text-decoration: none;
}
.kb-row:hover { background: var(--kb-border); }
.kb-row:focus-visible { outline: 2px solid var(--kb-brand-primary); outline-offset: -2px; }
.kb-row__icon { flex: none; display: inline-flex; color: var(--kb-muted); }
/* Truncates rather than wrapping, for the same reason the tree's rows do: a
   uniform row height is what makes a long list scannable. */
.kb-row__title {
  flex: 1;
  min-width: 0;
  font-size: .95rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kb-row__date { flex: none; color: var(--kb-muted); font-size: .8rem; }

/* The page tree. Filled by scripts/nav.js from nav.json — empty in the served
   HTML, so none of this applies to anything until that runs.

   Indentation is `--kb-depth` on the row rather than padding on the nested
   <ul>, so a row's hover and current-page background span the whole pane at
   every depth. A nested list indented by padding cannot do that: the
   background starts where the padding ends. */
.kb-nav__tree ul { list-style: none; margin: 0; padding: 0; }
.kb-nav__tree li { list-style: none; }

.kb-tree__row {
  display: flex;
  align-items: center;
  gap: .125rem;
  border-radius: .25rem;
  padding-left: calc(var(--kb-depth, 0) * .875rem);
}
.kb-tree__row:hover { background: var(--kb-border); }

.kb-tree__toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border: 0;
  border-radius: .1875rem;
  background: none;
  color: var(--kb-muted);
  cursor: pointer;
}
.kb-tree__toggle svg { transition: transform .15s ease; }
.kb-tree__toggle[aria-expanded='true'] svg { transform: rotate(90deg); }
.kb-tree__toggle:hover { color: var(--kb-fg); }
/* Leaves keep the chevron's footprint so every label in the pane starts on the
   same vertical line, which is what makes the depth readable at a glance. */
.kb-tree__toggle--leaf { cursor: default; }

.kb-tree__link {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .25rem .25rem .25rem .125rem;
  color: var(--kb-muted);
  text-decoration: none;
  font-size: .9rem;
  line-height: 1.35;
}
.kb-tree__icon { flex: none; display: inline-flex; color: var(--kb-muted); }
/* One row per page, truncated -- the app's tree rows are `truncate flex-1`
   and this is the same three declarations. A wrapped title costs the tree its
   scannability: the rows stop being a uniform height, so the eye can no
   longer take the depth of the hierarchy in at a glance, and a pane narrowed
   to its 180px minimum turns into a column of paragraphs.
   nav.js puts the untruncated title in a `title` attribute, so the part an
   ellipsis hides is still readable on hover and to a screen reader. */
.kb-tree__label {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.kb-tree__row:hover .kb-tree__link { color: var(--kb-fg); }
.kb-tree__link[aria-current='page'] { font-weight: 600; color: var(--kb-brand-primary); }
.kb-tree__link[aria-current='page'] .kb-tree__icon { color: var(--kb-brand-primary); }
.kb-tree__row:has(.kb-tree__link[aria-current='page']) { background: var(--kb-border); }

/* While a client-side navigation is in flight. Deliberately only a cursor: the
   swap is one fetch of one small document, and a spinner that appears and
   disappears inside 80ms reads as a flicker rather than as progress. */
[data-kb-loading] { cursor: progress; }

/* Search */
.kb-visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* The in-page search input these styled is gone: the header's box is the
   search page's input now, so there is one box on screen and it is in the
   same place on every page. `.kb-header__search-*` styles it. */
.kb-search__scope { margin: .75rem 0 0; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
/* "Search all of <Org>". Only rendered on the team host, where an org root
   exists above this community — see the comment on `orgSearchHref`. */
.kb-search__widen { font-size: .8125rem; color: var(--kb-brand-primary); }
.kb-chip {
  display: inline-block; padding: .15rem .6rem; border-radius: 999px;
  font-size: .8125rem; background: var(--kb-brand-accent); color: #111212;
}
.kb-search__status { margin: 1rem 0 0; color: var(--kb-muted); font-size: .9rem; }
.kb-search__results { list-style: none; margin: 1rem 0 0; padding: 0; }
.kb-search__results li { padding: .85rem 0; border-bottom: 1px solid var(--kb-border); }
.kb-search__hit { font-weight: 600; color: var(--kb-brand-primary); text-decoration: none; }
.kb-search__hit:hover { text-decoration: underline; }
.kb-search__excerpt { margin: .3rem 0 0; color: var(--kb-muted); font-size: .9rem; }
.kb-search__excerpt mark { background: var(--kb-brand-accent); color: inherit; }

/* Quieter than .kb-ask__button on purpose: Ask AI is this page's one call to
   action, and this is the continuation of a list already being read. */
.kb-search__more {
  margin: 1rem 0 0;
  background: transparent;
  color: var(--kb-brand-primary);
  border: 1px solid var(--kb-border);
  border-radius: 4px;
  padding: .5rem .9rem;
  font: inherit;
  cursor: pointer;
}
.kb-search__more:hover { border-color: var(--kb-brand-primary); }
.kb-search__more:focus-visible { outline: 2px solid var(--kb-brand-primary); outline-offset: 2px; }
.kb-search__more[disabled] { opacity: .6; cursor: default; }

/* The full page tree, static. Rendered ONLY inside the community index's
   <noscript>, so these rules apply exclusively to a reader with scripting off
   — for everyone else the tree in the left pane is the same information, and
   this used to be a third copy of it on the same screen. */
.kb-outline { margin-top: 3rem; border-top: 1px solid var(--kb-border); padding-top: 1.5rem; }
.kb-outline__heading {
  font-size: .8125rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--kb-muted); margin: 0 0 .75rem;
}
.kb-outline ul { list-style: none; margin: 0; padding: 0; }
.kb-outline__item { padding-left: calc(var(--kb-depth, 0) * 1.25rem); line-height: 1.9; }

.kb-empty { color: var(--kb-muted); }

/* THE FRONT DOOR'S CATEGORY CARDS.

   Their own classes rather than a modifier on `.kb-card`, because that class
   is also the community index's top-level pages and a folder page's children
   -- three surfaces, one of which is being restyled. A `.kb-card--category`
   would have put the shared rules one specificity slip away from all three.

   `--kb-category-color` is set per card by OrgHome.astro, cycling the classic
   palette by position, so the colour is data and this file is the shape. */
.kb-main--home { text-align: center; }
.kb-categories__heading {
  margin: 1rem 0 2rem;
  font-size: 1.35rem;
  font-weight: 600;
}
.kb-categories {
  /* The classic's own measure. Narrower than the shell so three cards make a
     row rather than stretching to whatever the window is. */
  max-width: 860px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
  /* Flex, not grid, and the reason is the count. `auto-fill` keeps the empty
     tracks, so a tenant with two communities got two cards pushed against the
     left of a three-column row; `auto-fit` drops them but then stretches two
     cards across the full measure, which is a pair of slabs. A wrapping
     centred row is right at every count: three fill the measure as the classic
     did, two sit centred under the heading, eight wrap into neat rows. */
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem;
}
.kb-category { flex: 0 1 15rem; }
.kb-category__link {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .75rem;
  height: 100%;
  padding: 2.5rem 1.5rem;
  border: 1px solid var(--kb-border);
  border-top: 3px solid var(--kb-category-color, var(--kb-brand-primary));
  /* The same .5rem the community's folder cards use, so every card on the
     site reads as one component. The coloured top border curves with the
     corners -- the classic KB's square cards predate the rest of this UI. */
  border-radius: .5rem;
  background: var(--kb-bg);
  color: inherit;
  text-decoration: none;
}
.kb-category__link:hover { background: var(--kb-border); }
.kb-category__link:focus-visible {
  outline: 2px solid var(--kb-category-color, var(--kb-brand-primary));
  outline-offset: 2px;
}
.kb-category__icon { color: var(--kb-category-color, var(--kb-brand-primary)); }
.kb-category__title { font-size: 1rem; font-weight: 600; }
.kb-category__meta { color: var(--kb-muted); font-size: .82rem; }

.kb-footer {
  background: var(--kb-brand-accent);
  padding: .75rem 1.5rem; font-size: .82rem;
  /* Always at the bottom of the window, and it takes nothing to arrange: the
     body is a column exactly one window tall that does not scroll, and this
     is its last item. The `position: sticky; bottom: 0` this replaced, and
     the header-and-footer measuring that went with it, were both answers to
     the document scrolling -- which it no longer does. */
}
.kb-footer__inner {
  max-width: var(--kb-max-width);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
/* Attribution left, share marks right -- the two halves the classic KB had.
   The tenant's own name used to sit between them and is gone: the banner above
   already says whose site this is, on every page. */
.kb-footer__share {
  display: flex;
  align-items: center;
  gap: .75rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
/* inline-flex so the link's box is the icon rather than the taller line box
   around it, which is what makes the row of marks line up with the text. */
.kb-footer__share-link { display: inline-flex; color: inherit; opacity: .75; }
.kb-footer__share-link:hover { opacity: 1; }
.kb-footer__share-link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  border-radius: .125rem;
  opacity: 1;
}

/* The way back to the tree, on the screens where the tree is not shown.
   Hidden by default: from 768px up the tree is in the pane beside this. */
.kb-shell__back {
  display: none;
  align-items: center;
  gap: .25rem;
  flex: none;
  padding: .75rem 1rem .25rem;
  color: var(--kb-muted);
  font-size: .9rem;
  text-decoration: none;
}
.kb-shell__back:hover { color: var(--kb-fg); }
.kb-shell__back-icon { flex: none; }

/* BELOW 768px THERE IS NO STACKED VIEW -- and 768px because that is where the
   app puts the same boundary (useBreakpoint's `mobile`; its `tablet` and
   `desktop` both get two panes, so one breakpoint covers both here).
   Stacking a tree on top of an article gives a reader two half-views and
   makes them scroll past all of one to reach the other, so instead exactly
   one pane is on screen at a time, as in CommunityPagesShell:
     - on the community root the TREE is the view;
     - on a page the tree is gone and `.kb-shell__back` replaces it. */
@media (max-width: 47.9375rem) {
  .kb-shell { flex-direction: column; }
  /* There is no horizontal split to drag when only one pane is on screen. */
  .kb-resizer { display: none; }
  .kb-main { padding: 1rem; }

  [data-kb-mode='root'] .kb-nav {
    /* Overrides --kb-nav-width, which resizer.js sets on <html> from a width
       the reader chose for a pane that does not exist at this size. */
    width: auto;
    flex: 1;
  }
  /* The cards duplicate the tree, and the tree is now the whole view -- but
     they go only once there IS a tree to replace them. nav.js sets
     data-kb-nav-ready after it paints; with no JavaScript it paints nothing,
     and the cards plus the <noscript> outline are the only navigation the
     page has. Removing them unconditionally would leave a blank page. */
  [data-kb-nav-ready] [data-kb-mode='root'] .kb-main { display: none; }

  [data-kb-mode='detail'] .kb-nav { display: none; }
  [data-kb-mode='detail'] .kb-shell__back { display: flex; }
}

/* Ask AI.
   A visibly distinct block rather than another search result: an answer is not
   a page on this site, and a reader has to be able to tell at a glance. */
.kb-ask { margin: 1.25rem 0 1.75rem; }
.kb-ask__button {
  background: var(--kb-brand-primary);
  color: #fff;
  border: 0;
  border-radius: 4px;
  padding: .5rem .9rem;
  font: inherit;
  cursor: pointer;
}
.kb-ask__button:focus-visible { outline: 2px solid var(--kb-brand-primary); outline-offset: 2px; }
.kb-ask__button[disabled] { opacity: .6; cursor: default; }
.kb-ask__note { margin: .5rem 0 0; font-size: .8125rem; opacity: .75; }
.kb-ask__panel {
  margin-top: 1rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--kb-brand-accent);
  border-left: 3px solid var(--kb-brand-primary);
  border-radius: 6px;
}
.kb-ask__status { margin: 0; font-size: .875rem; opacity: .8; }
.kb-ask__answer p { margin: 0 0 .75rem; }
.kb-ask__answer p:last-child { margin-bottom: 0; }
.kb-ask__answer ul { margin: 0 0 .75rem; padding-left: 1.2rem; }
.kb-ask__answer code { font-size: .9em; background: var(--kb-brand-accent); padding: 0 .2em; border-radius: 3px; }
.kb-ask__sources { margin: .75rem 0 0; padding-left: 1.1rem; font-size: .875rem; }
.kb-ask__sources a { color: var(--kb-brand-primary); }

/*
 * Classic-content parity layer for raw Post.html.
 *
 * PageDoc.astro inserts Post.html verbatim via set:html inside
 * `.aa-doc-body` — the exact same raw-HTML path the modern React app uses for
 * post bodies and v0 (pre-Plate) pages, because that content cannot go
 * through the Plate renderer. So the app's own `.aa-doc-body` layer is the
 * definition of how this content is supposed to look, and this file is a
 * MECHANICAL port of it: every rule below is the app's rule, selector and
 * declarations unchanged, from the ".aa-doc-body" section of
 * frontend/src/app/globals.css.
 *
 * It is mechanical because the previous version was not. It was written by
 * hand from the same source and had drifted in six places -- lists indented
 * twice (a `padding-left` on top of the app's `margin-left`), tables forced
 * full-width and left-aligned where the app centres them shrink-to-fit,
 * `pre` with different padding, radius and font-size, headings carrying a
 * `line-height` the app does not set, and body links in the customer's brand
 * colour rather than the app's link colour. None of those is visible unless
 * you put the two side by side, which is exactly why they survived.
 *
 * KEEPING IT MECHANICAL: __tests__/content-parity.test.ts re-parses both
 * files and fails on any rule that is present in one and not the other, or
 * whose declarations differ. Two kinds of exception are declared there by
 * name -- rules that are meaningless here, and the one divergence documented
 * inline below. Everything else has to match, so the way to change this file
 * is to change the app's and re-run the test.
 *
 * DO NOT "improve" a value here. A better line-height for a docs site is a
 * change to globals.css, which is where both surfaces read it from.
 */

/*
 * The tokens the ported rules reference, verbatim from the `:root` block of
 * globals.css.
 *
 * Copied rather than re-expressed in this package's own `--kb-*` names, for
 * the same reason the rules are copied: a hand-mapped `--kb-border` for
 * `--aa-border` is a value that can drift silently, and the test above can
 * only compare declarations that are textually the same.
 *
 * LIGHT ONLY. The app defines a `.dark` variant for all of these; this
 * artifact has no dark mode at all yet (`theme.colorScheme` is carried in the
 * manifest and not yet read), so adding half of one here would produce a
 * document with dark body text on a light ground or the reverse. When dark
 * mode arrives it is these eleven tokens plus site.css's `--kb-*` set.
 */
.aa-doc-body {
  --aa-text: 222 47% 11%;
  --aa-text-muted: 215 16% 47%;
  --aa-bg-soft: 210 20% 98%;
  --aa-border: 220 13% 91%;
  --aa-accent: 220 91% 54%;
  --foreground: 222.2 84% 4.9%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;
  --accent: 210 40% 96.1%;
  --border: 214.3 31.8% 91.4%;
  --card: 0 0% 100%;
}

/* ===================================================================
 * Ported verbatim from frontend/src/app/globals.css below this line.
 * =================================================================== */

/* === aa-doc-body: rendered Django page content === */
.aa-doc-body {
  font-size: 15px;
  line-height: 1.65;
  color: hsl(var(--aa-text));
}

/* === The baseline the rules below stand on ===
 *
 * Everything under `.aa-doc-body` used to assume Tailwind PREFLIGHT had
 * already run. In this app it always has, so the assumption was invisible --
 * and the rules below were written against a normalized starting point that
 * the stylesheet itself never states.
 *
 * That made the layer un-portable, and it IS ported: kb/src/styles/content.css
 * is a mechanical copy of this section (kb/src/__tests__/content-parity.test.ts
 * compares them rule by rule), because the public KB renders the same raw
 * Post.html and has no preflight. Every rule the port was missing showed up as
 * a real defect on the published site, measured against this app side by side:
 * `<audio>`/`<video>` laid out INLINE so three players sat in a row instead of
 * stacking; `<iframe>` likewise, with the UA's 2px inset border around every
 * embed; lists indented 40px per level ON TOP of the 1.5rem below, compounding
 * with nesting; `<hr>` as the UA's 2px grooved line; `<figure>` and `<dd>`
 * inset 40px; `<pre>` in the generic `monospace` face rather than the stack.
 *
 * So the rules are restated here, scoped, rather than left implicit. Each one
 * is a NO-OP in this app -- it says what preflight already computes, verified
 * by measuring both surfaces before and after -- and load-bearing anywhere
 * else. The point is that this section is now a complete specification of how
 * a rendered body looks, which is what the other surface reads it as.
 *
 * Deliberately NOT restated, because no `Post.html` can contain the elements
 * they target: the form-control block (button/input/optgroup/select/textarea,
 * fieldset, legend, dialog, placeholders, the -webkit- pseudo-elements),
 * `html`/`body`, and the focus-ring rules. `[hidden]` is omitted too -- every
 * UA does it already. `a` is omitted because `.aa-doc-body a` below overrides
 * both of preflight's declarations outright, so it can have no effect.
 *
 * The container ITSELF is in the first selector, not only its descendants:
 * preflight's `*` matches `.aa-doc-body` as well, and `border-color: inherit`
 * on the table rule reads its PARENT's. Scoped to descendants alone, a table
 * inherited the text colour instead -- invisible while every bordered rule
 * also names a colour, and waiting for the first one that does not.
 */
.aa-doc-body,
.aa-doc-body *,
.aa-doc-body ::before,
.aa-doc-body ::after {
  box-sizing: border-box;
  border-width: 0;
  border-style: solid;
  border-color: #e5e7eb;
}
.aa-doc-body blockquote, .aa-doc-body dl, .aa-doc-body dd, .aa-doc-body h1,
.aa-doc-body h2, .aa-doc-body h3, .aa-doc-body h4, .aa-doc-body h5,
.aa-doc-body h6, .aa-doc-body hr, .aa-doc-body figure, .aa-doc-body p,
.aa-doc-body pre {
  margin: 0;
}
.aa-doc-body ol, .aa-doc-body ul, .aa-doc-body menu {
  list-style: none;
  margin: 0;
  padding: 0;
}
.aa-doc-body h1, .aa-doc-body h2, .aa-doc-body h3, .aa-doc-body h4,
.aa-doc-body h5, .aa-doc-body h6 {
  font-size: inherit;
  font-weight: inherit;
}
.aa-doc-body hr { height: 0; color: inherit; border-top-width: 1px; }
.aa-doc-body abbr:where([title]) { text-decoration: underline dotted; }
.aa-doc-body b, .aa-doc-body strong { font-weight: bolder; }
.aa-doc-body code, .aa-doc-body kbd, .aa-doc-body samp, .aa-doc-body pre {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 1em;
}
.aa-doc-body small { font-size: 80%; }
.aa-doc-body sub, .aa-doc-body sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}
.aa-doc-body sub { bottom: -0.25em; }
.aa-doc-body sup { top: -0.5em; }
.aa-doc-body summary { display: list-item; }
/* `img` is restored to inline-block further down -- see the note there. The
   others stay block, which is what stacks a player instead of floating it into
   the text run. */
.aa-doc-body img, .aa-doc-body svg, .aa-doc-body video, .aa-doc-body canvas,
.aa-doc-body audio, .aa-doc-body iframe, .aa-doc-body embed,
.aa-doc-body object {
  display: block;
  vertical-align: middle;
}
.aa-doc-body video { max-width: 100%; height: auto; }
.aa-doc-body h1 {
  font-size: 1.75rem; font-weight: 700; margin: 1.5rem 0 0.75rem;
}
.aa-doc-body h2 {
  font-size: 1.4rem; font-weight: 600; margin: 1.25rem 0 0.5rem;
}
.aa-doc-body h3 {
  font-size: 1.15rem; font-weight: 600; margin: 1rem 0 0.5rem;
}
/* h4-h6 continue the scale above. They need saying: preflight resets EVERY
   heading to the body's size and weight, and only h1-h3 were given it back --
   so an <h4> in a post body has been rendering as an ordinary paragraph, which
   the classic editor (no preflight, UA bold) never did. */
.aa-doc-body h4 { font-size: 1.05rem; font-weight: 600; margin: 1rem 0 0.5rem; }
.aa-doc-body h5 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.5rem; }
.aa-doc-body h6 { font-size: 0.9rem; font-weight: 600; margin: 1rem 0 0.5rem; }
.aa-doc-body p {
  margin: 0.5rem 0;
}
.aa-doc-body ul,
.aa-doc-body ol {
  margin: 0.5rem 0 0.5rem 1.5rem;
}
.aa-doc-body ul {
  list-style: disc;
}
.aa-doc-body ol {
  list-style: decimal;
}
/* Legacy aae-toc (the classic editor's baked Table of Contents widget; v0 pages
   render it as raw HTML). Style it to match the modern Plate `toc` node
   (toc-node.tsx `TocElement`): no bullets, full-width muted underlined links
   with a hover highlight, depth indentation via the nested <ul>, and the empty
   spacer <li> entries (classic authors' blank `<h3>` headers) hidden — the
   modern toc node filters empty headings, so matching that here keeps parity.
   Without these rules the generic `.aa-doc-body ul { list-style: disc }` leaks
   stray bullets and the generic link style onto the TOC. */
.aa-doc-body .aae-toc {
  margin: 1rem 0;
}
/* The modern `toc` node renders no title — hide the classic baked
   "Table of Contents" heading so the raw-HTML TOC matches it. */
.aa-doc-body .aae-toc > h4 {
  display: none;
}
.aa-doc-body .aae-toc ul {
  list-style: none; margin: 0; padding-left: 0;
}
.aa-doc-body .aae-toc li {
  list-style: none; margin: 0;
}
.aa-doc-body .aae-toc li ul {
  padding-left: 1.5rem;
}
/* depth indentation */
.aa-doc-body .aae-toc li:not(:has(a)) {
  display: none;
}
/* hide empty spacers */
.aa-doc-body .aae-toc a {
  display: block;
  padding: 0.375rem 0.125rem;
  font-weight: 500;
  color: hsl(var(--muted-foreground));
  text-decoration: underline;
  text-decoration-thickness: 0.5px;
  text-underline-offset: 4px;
  border-radius: 0.25rem;
}
.aa-doc-body .aae-toc a:hover {
  background: hsl(var(--accent));
  color: hsl(var(--foreground));
}
/* Legacy aa-editor-checklist (editor_version=0 pages render raw HTML).
   Ported from the classic Django editor CSS (allanswered.editor.css):
   draw checkbox markers via ::before instead of falling back to the
   `ul { list-style: disc }` bullets above. The base64 SVGs are the
   unchecked (grey) and checked (teal) checkbox glyphs. */
.aa-doc-body ul.aa-editor-checklist {
  list-style: none;
}
.aa-doc-body ul.aa-editor-checklist > li {
  list-style: none;
  position: relative;
  min-height: 1.5em;
}
.aa-doc-body ul.aa-editor-checklist > li::before {
  content: '';
  position: absolute;
  margin-left: -1.5em;
  margin-top: 0.2em;
  width: 1em;
  height: 1em;
  background-size: 100%;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgo8dGl0bGU+Y2hlY2tib3gtdW5jaGVja2VkPC90aXRsZT4KPHBhdGggZmlsbD0iIzgwODA4MCIgZD0iTTI4IDBoLTI0Yy0yLjIgMC00IDEuOC00IDR2MjRjMCAyLjIgMS44IDQgNCA0aDI0YzIuMiAwIDQtMS44IDQtNHYtMjRjMC0yLjItMS44LTQtNC00ek0yOCAyOGgtMjR2LTI0aDI0djI0eiI+PC9wYXRoPgo8L3N2Zz4K");
}
.aa-doc-body ul.aa-editor-checklist > li.aa-editor-checklist-checked::before {
  background-image: url("data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgo8dGl0bGU+Y2hlY2tib3gtY2hlY2tlZDwvdGl0bGU+CjxwYXRoIGZpbGw9IiMzOWNjY2MiIGQ9Ik0yOCAwaC0yNGMtMi4yIDAtNCAxLjgtNCA0djI0YzAgMi4yIDEuOCA0IDQgNGgyNGMyLjIgMCA0LTEuOCA0LTR2LTI0YzAtMi4yLTEuOC00LTQtNHpNMTQgMjQuODI4bC03LjQxNC03LjQxNCAyLjgyOC0yLjgyOCA0LjU4NiA0LjU4NiA5LjU4Ni05LjU4NiAyLjgyOCAyLjgyOC0xMi40MTQgMTIuNDE0eiI+PC9wYXRoPgo8L3N2Zz4K");
}
.aa-doc-body code {
  font-family: ui-monospace, SFMono-Regular, monospace;
  background: hsl(var(--aa-bg-soft));
  padding: 0.1rem 0.3rem;
  border-radius: 3px;
  font-size: 0.92em;
}
.aa-doc-body pre {
  background: hsl(var(--aa-bg-soft));
  padding: 0.75rem;
  border-radius: 6px;
  overflow-x: auto;
  margin: 0.75rem 0;
}
.aa-doc-body pre code {
  background: transparent; padding: 0;
}
.aa-doc-body a {
  color: hsl(var(--aa-accent)); text-decoration: underline;
}
/* Tailwind preflight sets img { display: block } globally; restore inline so
   images in body text flow with surrounding text, matching classic behaviour. */
.aa-doc-body img {
  max-width: 100%; height: auto; display: inline-block; vertical-align: middle;
}
/* Inline images sit inside a paragraph alongside text — give them breathing room. */
.aa-doc-body p img {
  margin: 2px 8px;
}
/* Emoji images carry no width attribute — classic view sizes them via this
   class. Mirror that rule here so the raw-HTML fallback path matches. */
.aa-doc-body img.emoji-image {
  width: 20px; height: 20px;
}
.aa-doc-body blockquote {
  border-left: 3px solid hsl(var(--aa-border));
  padding-left: 0.75rem;
  margin: 0.75rem 0;
  color: hsl(var(--aa-text-muted));
}
.aa-doc-body table {
  text-indent: 0; border-color: inherit; border-collapse: collapse; margin: 0.75rem auto;
}
.aa-doc-body th,
.aa-doc-body td {
  border: 1px solid hsl(var(--aa-border));
  padding: 0.5rem 0.75rem;
  text-align: left;
}
/* Body mention pills (raw-HTML surfaces: posts + v0 pages). Match the Plate
   MentionElement pill (bg-muted) so classic-origin and modern mentions look
   identical. Covers the classic `rte-command-mention` anchor and the legacy
   serialized `.mention` span. The Plate-rendered pill already uses these
   tokens via Tailwind. */
.aa-doc-body a.rte-command-mention,
.aa-doc-body .mention {
  display: inline-block;
  background: hsl(var(--muted));
  color: hsl(var(--foreground));
  padding: 0.05rem 0.375rem;
  border-radius: 0.375rem;
  font-weight: 500;
  line-height: inherit;
  text-decoration: none;
}
.aa-doc-body a.rte-command-mention[href] {
  cursor: pointer;
}
.aa-doc-body a.rte-command-mention[href]:hover {
  background: hsl(var(--muted) / 0.7);
}
/* Legacy aaeditor template placeholders — gray hints used in pre-built
   templates (settings.EDITOR_TEMPLATES → /editor_templates/*.html). */
.aa-doc-body span.aa-editor-template-placeholder,
.aa-doc-body span.aa-editor-template-placeholder-current-date {
  color: hsl(var(--aa-text-muted));
  font-style: italic;
}
/* === Classic attachment chips — match the Plate file node (icon + chip) === */
.aa-doc-body .aa-uploaded-file,
.aa-doc-body .aa-editor-fileupload {
  display: inline-flex;
  align-items: center;
  /* Center the chip against the adjacent plain-text size suffix — an inline-flex
     box's synthesized baseline is pushed down by the taller centered icon, which
     otherwise makes "(803.18 KB)" sit lower than the filename. */
  vertical-align: middle;
  gap: 0.25rem;
  padding: 3px 0.375rem;
  border-radius: 0.25rem;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
.aa-doc-body .aa-uploaded-file:hover,
.aa-doc-body .aa-editor-fileupload:hover {
  background: hsl(var(--muted));
}
/* File-type icon box. The specific colored SVG comes from the generated
   src/app/file-type-icons.css (`.file-<ext>::before`); this base rule sizes the
   box and provides the default.svg fallback for any extension not in the set. */
.aa-doc-body .aa-uploaded-file::before,
.aa-doc-body .aa-editor-fileupload::before {
  content: "";
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  background: center / contain no-repeat;
  /* NO background-image, and this is the one KNOWN divergence from the app.
     The colored per-extension glyph comes from frontend/src/app/
     file-type-icons.css, backed by 405 SVGs under frontend/public/fileicons —
     assets on the APP's static origin, which this artifact deliberately does
     not depend on (it has to keep rendering on the customer's own domain with
     the app unreachable). Naming that URL here would give every reader a
     broken image instead of a missing one, and the app's own `default.svg`
     fallback is a placeholder with the word DEFAULT drawn into it, which is
     further from parity than nothing.
     The box is kept at its full size on purpose: the chip then occupies the
     same space and sits on the same baseline as the app's, so a page's LAYOUT
     is unchanged and only the glyph is absent. Shipping the real glyphs needs
     the icon set inside this package (the renderer image contains kb/ and
     nothing else) plus a per-page <style> of data URIs, since the shared
     hashed stylesheet cannot carry per-community rules and a root-relative
     URL is wrong under a base path. */
}
/* === Parity-audit fix-now rules (2026-06-20) — classic markup that had NO
   .aa-doc-body coverage, so it rendered unstyled (callout) or broken-layout
   (columns stacked vertically). Conservative first pass mirroring the Plate
   node look; needs a visual confirmation pass against the running app. === */
/* Callout — classic <div class="callout">, Plate target: flex rounded bg-muted p-4. */
.aa-doc-body .callout {
  display: flex;
  gap: 0.5rem;
  margin: 0.25rem 0;
  padding: 1rem 1rem 1rem 0.75rem;
  border-radius: 0.125rem;
  background: hsl(var(--muted));
}
/* The emoji is a callout's only variant signal, and every post/answer/comment
   body — plus any classic-editor page — renders from HTML rather than the
   read-only Plate view. The serializer emits it as data-icon; paint it here so
   those surfaces show the same thing the editor does. */
.aa-doc-body .callout[data-icon]::before {
  content: attr(data-icon);
  flex: none;
  font-size: 18px;
  line-height: 1.4;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji,
  "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols;
}
/* Media attachments — match the Plate audio/video nodes. A body carrying one
   renders raw HTML on three surfaces (a post, an answer, a comment, a v0 page,
   and the whole public KB) and Plate on one, and the serializer emits a bare
   `<audio src controls>` / `<video src controls>` -- so without this the same
   file is a full-width 64px player in the editor and a 300px browser default
   everywhere else.

   Measured off the nodes rather than chosen: media-audio-node.tsx wraps the
   player in `h-16 rounded-sm` at `size-full` inside a `mb-1` block, and
   media-video-node.tsx renders `rounded-sm` inside a `py-2.5` figure, centred,
   seeded at use-default-media-width's DEFAULT_MEDIA_WIDTH of 550.

   THE 550 IS A FALLBACK, NOT A CEILING. The serializer emits the resizable's
   width as an inline `style="width:..."`, which outranks this rule, so an
   author's resize reaches every raw-HTML surface. It only applies to a video
   whose HTML carries no width -- everything saved before that change, and
   anything a classic editor wrote without one. `max-width: 100%` in the
   baseline block above keeps either from overflowing a narrow column. */
.aa-doc-body audio {
  width: 100%;
  height: 4rem;
  margin-bottom: 0.25rem;
  border-radius: 0.25rem;
}
.aa-doc-body video {
  width: 550px;
  margin: 0.625rem auto;
  border-radius: 0.25rem;
}

/* Embedded video/player — `<div class="mediaWrapper"><iframe …></div>`, which
   is what the serializer emits for a media_embed node.

   THE CLASS HAD NO RULE ANYWHERE. It is the classic editor's wrapper and it
   was styled by scss/apps/_content.scss, deleted with the classic bundles
   (713a1d968 / 7b66d569f) — so since the decommission every embed has rendered
   at the literal `width="560" height="314"` on its iframe attributes: fixed,
   never scaled, and overflowing the body on any column narrower than 560px.
   The classic rule was a 16:9 responsive box and this restores that shape.

   `aspect-ratio` rather than the classic percentage-padding trick, because the
   two are not equivalent here: percentage padding resolves against the
   CONTAINING BLOCK's width, so `padding-bottom: 56.25%` next to a capped
   `max-width` produces a box whose height tracks the article column while its
   width does not — the ratio is only right when the cap happens not to bite.
   `height: auto` is load-bearing: it is what overrides the `height="314"`
   presentational hint the serializer writes.

   560 is the serializer's own width attribute, and matches what the app shows
   — `useDefaultMediaWidth` seeds a media node at 550 and Plate renders it
   there, so an embed is a similar size on both surfaces rather than
   full-bleed on one of them. */
.aa-doc-body .mediaWrapper {
  max-width: 560px;
  margin: 0.625rem auto;
}
.aa-doc-body .mediaWrapper > iframe {
  width: 100%;
  height: auto;
  aspect-ratio: 560 / 314;
  border: 0;
  border-radius: 0.25rem;
}

/* Classic "embed an external page" iframe. The classic editor wrote this class
   together with a `height="100vh"` ATTRIBUTE, and the two only work as a pair:
   `100vh` is not a valid HTML dimension, and the rules for parsing dimension
   values take the leading digits and drop the rest, so the attribute alone
   computes to `height: 100px`. The height that actually rendered came from
   scss/apps/_content.scss (and its copy in _external.scss), deleted with the
   classic bundles in 713a1d968 / 7b66d569f:

     iframe.aa-external-iframe {
       width: 90%; margin-left: 5%; margin-top: 10px; height: 100vh;
     }

   Restated verbatim rather than re-chosen. The pixel `height=` attribute on
   one of these has NEVER rendered -- a class rule outranks a presentational
   hint, so this rule always won -- which means an author's stored `533` is a
   number nobody has ever seen take effect, and honouring it now would not be
   preserving their intent. Every such embed was written and approved at
   viewport height.

   Live data as of 2026-09-15 (build-reuse, 57 pages): two bodies carry the
   class, both as the LAST element in the page, which is also why a viewport-
   tall frame pushes no article text below a nested scroller here. */
.aa-doc-body iframe.aa-external-iframe {
  width: 90%;
  margin-left: 5%;
  margin-top: 10px;
  height: 100vh;
}
/* Columns — classic <div class="columns"><div class="column">. Without flex the
   columns stack vertically (broken). Equal-width by default; an inline width
   style on a .column (when present) overrides flex-basis. */
.aa-doc-body .columns {
  display: flex;
  gap: 1rem;
}
.aa-doc-body .columns > .column {
  flex: 1 1 0%;
  min-width: 0;
}
/* Expandable Section (classic accordion markup) on the post read path. Native
   <details>/<summary> handles collapse; this matches the modern neutral design. */
.aa-doc-body details.aae-accordion {
  border: 1px solid hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--card));
  margin: 8px 0;
  overflow: hidden;
}
.aa-doc-body details.aae-accordion > summary {
  list-style: none;
  cursor: pointer;
  padding: 9px 12px;
  font-size: 14px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 8px;
}
.aa-doc-body details.aae-accordion > summary::-webkit-details-marker {
  display: none;
}
.aa-doc-body details.aae-accordion > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid hsl(var(--muted-foreground));
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform .18s ease;
  flex-shrink: 0;
}
.aa-doc-body details.aae-accordion[open] > summary::before {
  transform: rotate(90deg);
}
.aa-doc-body details.aae-accordion > .aae-accordion-body {
  padding: 2px 14px 14px 34px;
}
.aa-doc-body details.aae-accordion:hover > summary {
  background: hsl(var(--muted));
}

/* ===================================================================
 * KB-ONLY, below this line. Everything above is the app's.
 *
 * The parity test knows this section is here and does not require the app to
 * contain any of it -- but it does require anything that LOOKS like a ported
 * rule to be above the line, so a future edit cannot quietly land a
 * divergence down here.
 * =================================================================== */

/* Headings inside table cells. The app's rule for this is spelled
   `table td h1` with `!important` on every declaration, because there it has
   to beat the Tailwind utility classes Plate puts on a heading (text-4xl,
   mt-[1.6em], pb-1). Nothing here emits those classes, so the rule is scoped
   to this layer and the `!important`s are dropped; the VALUES are the app's.
   It is down here rather than above the line because it is the one rule whose
   text legitimately differs. */
.aa-doc-body table td h1, .aa-doc-body table th h1 { font-size: 1.75rem; font-weight: 700; margin: 1.5rem 0 0.75rem; padding-bottom: 0; }
.aa-doc-body table td h2, .aa-doc-body table th h2 { font-size: 1.4rem; font-weight: 600; margin: 1.25rem 0 0.5rem; padding-bottom: 0; }
.aa-doc-body table td h3, .aa-doc-body table th h3 { font-size: 1.15rem; font-weight: 600; margin: 1rem 0 0.5rem; padding-bottom: 0; }

/* A word with no break opportunity -- a long URL pasted as text, a Windows
   share path -- is wider than this column at any window size, and the reader
   has no horizontal scrollbar on the article to reach the end of it. The app
   overflows here too; it is not a format difference, because it changes
   nothing on content that fits, and on content that does not fit the
   alternative is a page the reader cannot read. */
.aa-doc-body { overflow-wrap: break-word; }

/* Rich-embed cards, which exist ONLY on this surface.
   The app has no rule for either of these classes and never will: it paints a
   card from React (`rich-embed-card.tsx`) with Tailwind utilities on the
   element, from JSON fetched at read time. A static site cannot do that, so
   `kb.links.rewrite_embed_cards` bakes the card into the published HTML at
   publish time and this is the stylesheet for what it bakes. That is why the
   rules are below the line rather than ported -- there is nothing upstream to
   port, and adding one to globals.css to satisfy the parity test would be dead
   CSS in the app.

   The VALUES are the app's card, read off those utility classes: border
   rounded-md p-3, medium-weight title. The app's description and thumbnail are
   deliberately absent -- the card carries the target's title and nothing else,
   so there is no second line to lay out and no row to build. */
.aa-doc-body a.aa-rich-embed-card {
  display: block;
  margin: 0.75rem 0;
  padding: 0.75rem;
  border: 1px solid hsl(var(--aa-border));
  border-radius: 0.375rem;
  /* Both override `.aa-doc-body a` above, which underlines in the accent
     colour. A whole card underlined in blue reads as broken markup. */
  color: hsl(var(--aa-text));
  text-decoration: none;
}
.aa-doc-body a.aa-rich-embed-card:hover {
  background: hsl(var(--aa-bg-soft));
}
/* A long title truncates to the column rather than wrapping the box open. */
.aa-doc-body .aa-rich-embed-card-title {
  display: block;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A card with no public form, published as the link it degrades to.
   BLOCK, because the placeholder it replaces was a block: a div between two
   blocks. Inline would pull the URL up into the end of the preceding
   paragraph. It keeps the ordinary body-link colour and underline -- it IS an
   ordinary link, and dressing it as a card would promise metadata that is not
   there. `min-width: 0` is what lets the ellipsis below actually apply inside
   a flex or grid ancestor. */
.aa-doc-body a.aa-rich-embed-link {
  display: block;
  min-width: 0;
  margin: 0.75rem 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * === Structurally-present, no visual meaning ===
 *
 * Classes that show up in real harvested bodies but carry no styling intent
 * of their own — kept here (rather than left unstyled, which the parity
 * test would also catch) with a comment so it's clear the omission is a
 * decision, not an oversight.
 */

/* Google-Docs paste artifact: an arbitrary per-document hash class that
   Google's clipboard HTML attaches to pasted spans (this exact name is
   whatever Google Docs generated for one author's paste — there is no
   shared meaning across documents, and no stylesheet anywhere in this
   codebase — classic, modern, or here — has ever had a rule for it). Left
   intentionally unstyled; this empty rule exists only so the parity test
   records that it was seen and considered, not missed. If more such hashes
   turn up in later fixtures, add them here rather than inventing a real
   rule for noise. */
.aa-doc-body .-KXLs {
}
