/*SECTION - Components */
.js-tabs,
.js-toggles {
  visibility: hidden;
}

/* SECTION - Modals */
#dialog-content {
  display: none;
  /* Was `clamp(23ch, 50%, 46ch)`. Pinned to rem on 2026-10-25 -- `ch` is
     typeface-dependent and the Inter swap widened it 10.3%. See
     css-utilities.css:750 for the full reason. Nothing sets a font-size on this
     element or above it, so its `ch` was Open Sans at 16px (9.1486px): 23ch was
     210.4px and 46ch was 420.8px. */
  width: clamp(13.15rem, 50%, 26.3rem);
  flex-direction: column;
  gap: 1.5rem;
  padding: 1.5rem;
  border-radius: 0.5rem;
}
/* !SECTION - Modals */

/* SECTION - Tabs */
/* Restyled to the Codex tab pattern on 2026-10-28. This is a MIGRATION, not a
   recolour: every surface in this block was a legacy --clr-* primitive with no
   dark tier, so a dark-mode reader got a white tab strip and a white panel
   sitting on the dark article card -- #ffffff on #1e2a35, 16.47:1 of pure glare
   -- and css-utilities.css had to hold .tab-inner's ink dark to keep it legible
   on the white it never stopped being. That patch entry is gone now; .tab-inner
   was the last of its four. Same bug class as the article tables (2026-10-27)
   and the accordion (2026-10-28).

   THE SHAPE, sampled at native resolution off Joy's light and dark mockups
   rather than read off Codex's CSS, which we cannot reach from here:

     - NO BOXES. Tabs have no borders, and the panel has no border, fill or
       radius at all. One 1px rule runs along the foot of the strip, and the
       active tab replaces its own segment of that rule with a 2px brand bar.
       The sampled bar and rule share a bottom edge with the bar standing 1px
       proud, which is what .tab-item's -1px margin reproduces.
     - THE FILL IS CODEX'S HOVER STATE AND OUR RESTING ACTIVE STATE. Joy,
       2026-10-28: "In codex the active tab only has the brand color underline,
       and the background color only appears on hover. However, I would like the
       active tab to have the background color as well. When hovered we can do a
       slightly darker background." That is the same resolution the accordion
       took -- the hover colour became the open state, with a slight further
       darkening separating them.
     - THE FILL BELONGS TO THE ACTIVE TAB ALONE, which is a SECOND decision from
       Joy the same day and narrows the one above: hovering an INACTIVE tab does
       NOT fill, it only darkens that tab's segment of the baseline rule and
       lifts the label's ink. So --tab-fill is used by .is-active and nothing
       else, and --tab-fill-strong only by the active tab hovered. The full
       reasoning, including why the hovered rule is allowed under 3:1, is at the
       `&:hover:not(.is-active)` rule below.

   WHY THE BAR TIER-SPLITS WHEN NOTHING ELSE DOES. Codex's tabs sit on the page;
   ours sit inside .article-container. In light those are the same colour
   (#faffff), so the mockup transfers one-to-one. In dark the page is #10171e and
   the card is #1e2a35, so --neutral-bg-soft #172029 -- the token matching
   Codex's sampled fill at ΔE00 0.79 -- lifts off Codex's page but SINKS into our
   card. Keeping the raised look the dark mockup actually shows means a lifted
   fill, and against a lifted fill --brand-bg #d73312 fails the 3:1 non-text
   floor: 2.20:1 on the alpha wash. --brand-border-STRONG is the token that
   carries a dark override, to #f8694c, and clears the floor at 3.59:1 -- not
   --brand-border, which has no dark tier at all. Getting those two the wrong way
   round is the trap documented at the dark-tier block below; the authority on
   which token the bar takes in each tier is that block, not this paragraph. So
   the bar is the only thing in this block with two tiers, and it is a collision
   that forces it rather than a preference. Codex itself uses one orange in both
   tiers, which is what --brand-bg does -- the split is the cost of our surface
   being a card.

   MEASURED, light / dark:
     active label on the fill            16.60:1 / 9.23:1    AAA both
     active label on the strong fill     13.58:1 / 14.35:1   AAA both
     inactive label on the card           6.61:1 / 8.09:1    AA light, AAA dark
     hovered inactive label on the card  17.89:1 / 12.73:1   AAA both
     brand bar on the fill                 4.42:1 / 3.59:1   clears 3:1
     brand bar on the strong fill          3.61:1 / 5.58:1   clears 3:1
     baseline rule on the card             1.47:1 / 1.33:1   decorative, see below

   The hovered-inactive row is --neutral-text on the bare card, NOT on a fill --
   that state has no fill, per the second bullet above. It is the only label
   state that is measured against the card rather than against a tab surface.

   THE BASELINE RULE IS BELOW 3:1 IN BOTH TIERS AND THAT IS FINE, because 1.4.11
   governs boundaries REQUIRED to identify a component or its state, and this one
   identifies nothing: the tabs are named by their labels, and the active one is
   marked by a fill and a bar that both clear the floor on their own. This is the
   same argument that lets the accordion's card edge stand. It is also why the
   old design failed where this one does not -- a --clr-neutral-40 border at
   1.78:1 WAS the only thing separating one tab from the next.

   THE INACTIVE LABEL IS AA, NOT AAA, IN LIGHT (6.61:1), and it is deliberate.
   AAA is the target wherever it is reachable, but reaching it here means
   --neutral-text, which is the active label's ink -- it would erase the one
   thing the muted ink exists to say. Logged in plan.md's Phase 1 deviations.

   SPACING IS THE ONE THING HERE THAT IS NOT A SURFACES CHANGE. The restyle
   itself touched no padding -- the spacing pass had closed on 2026-10-28 -- and
   the two geometry edits it did make (.tab-item's vertical padding and its -1px
   margin) are consequences of the design rather than spacing decisions, noted at
   their own rules. THEN Joy reopened .tab-link's padding on 2026-10-29 and put
   it on the fluid scale; that is the single deliberate exception, it is argued
   in full at the rule, and it does NOT generalise to the panel. .tab-inner's
   padding and gap are still exactly as found.

   TYPE IS UNTOUCHED. The labels carry no font-size and inherit --step-0, which
   is what the mockups show; only the active WEIGHT is stated, because the
   mockups show that too. If a type ladder is ever wanted here, note the only
   font-size in this block has always been on .nav-tabs .nav-link, which matches
   nothing -- see the dead rules below. */
.nav-tabs {
  /* THE TWO INTERACTIVE FILLS AND THE BAR ARE NAMED HERE AND NOWHERE ELSE, so
     the dark tier is one override instead of five, and so the contrast figures
     above stay attached to the values they were measured on. */
  --tab-fill: var(--neutral-bg-soft);
  --tab-fill-strong: var(--neutral-bg-component-hover);
  /* --brand-border, NOT --brand-bg, and this corrects a mis-attribution worth
     recording: I had measured the mockup's underline at #dd5334 and logged it as
     a ΔE00 6.51 DEVIATION from --brand-bg #d73312. It was never a deviation --
     Codex's .kb-tab-indicator names `background-color: var(--brand-border)`
     outright, and --brand-border is #ef4523, ΔE00 3.48 from that sample on a 2px
     antialiased line. I had matched the right colour to the wrong token. The
     mis-reading never reached plan.md -- it was caught while the tab work was
     still uncommitted, so there is no deviation entry there to withdraw, and
     nothing should be added for it. This paragraph is the whole record.

     Measured as the selected-state boundary, which DOES owe 3:1: 3.75:1 on the
     card and 3.48:1 over the active tab's own fill. */
  --tab-bar: var(--brand-border);

  position: relative;
  /* NO GAP. Codex's tabs butt right up against each other so the strip reads as
     one continuous surface, and the hover fill of one tab meets the next.

     I briefly had 8px here, misreading Codex's
     `gap: calc(var(--spacing) * 2)`: that declaration is on the TRIGGER, where
     it spaces a trigger's own icon from its own label, not one tab from the
     next. Our tabs are text-only so there is nothing for it to space yet --
     if icons ever land in a tab, 8px goes on .tab-link as a flex gap, not
     here. */
  gap: 0;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  /* The baseline rule the whole strip sits on. The sliding indicator below
     covers whichever segment of it belongs to the active tab, and a hovered
     inactive tab darkens its own segment with .tab-link's border -- both pulled
     into place by .tab-item's negative margin. `position: relative` above is
     the indicator's containing block, not decoration. */
  border-block-end: var(--border-width-regular) solid var(--neutral-border-soft);
  /* `overflow: hidden` WAS HERE AND HAD TO GO, because the -1px overhang that
     puts the bar on the rule is exactly what it would clip. Removing it also
     un-clips the sitewide focus ring on .tab-link, which was the one real entry
     in the clipped-ring table at css-reset.css:54 -- that row is updated. The
     template carried an `overflow-hidden` utility class on this element saying
     the same thing in markup; it is removed there too, or this declaration's
     absence would mean nothing. */
}

/* THE DARK TIER CANNOT REUSE THE LIGHT FILLS, for the same collision the
   accordion documented at :415 -- --neutral-bg-soft inverts direction against
   our card (see the header above), and the obvious lift,
   --neutral-bg-component #2e3e4e, is EXACTLY --neutral-border-soft, so an
   active tab's fill would be the rule's own colour and the rule would vanish
   behind it. These are the accordion's two dark values, chosen again here for
   the same reasons and re-measured against the bar rather than a divider:
   --neutral-bg-alpha is a 10.2% white wash compositing to #35404a over the card
   (9.23:1 ink, ΔE00 7.12 from the card), and the strong fill goes DARKER to
   #172029 (14.35:1 ink, ΔE00 10.12 from the fill), which is still the darkening
   Joy asked for and is also the direction that keeps the bar healthiest.
   Do not "fix" the alpha to a solid token without re-measuring the bar
   against it.

   THE BAR IS THE ONE VALUE THAT MUST SPLIT BY TIER, and the reason is a token
   trap I fell into first time round. --brand-bg and --brand-border have NO dark
   override -- the dark block at css-brand-tokens.css:998 overrides
   --brand-border-STRONG (#992008 light -> #f8694c dark) and leaves the other two
   alone. So `var(--brand-border)` here would have quietly stayed #ef4523 in
   dark, which measures 2.80:1 over the alpha fill and FAILS the 3:1 this
   boundary owes. #f8694c is the value I had actually measured at 3.59:1 and
   mislabelled as --brand-border; it is --brand-border-strong, so that is what
   this says. Result: 3.59:1 over the alpha fill, 5.58:1 over the strong fill,
   4.95:1 on the bare card. Light stays on --brand-border because
   --brand-border-strong there is #992008, a dark maroon ΔE00 18.81 from the
   mockup's coral -- right contrast, wrong colour. */
:root[data-theme='dark'] .nav-tabs {
  --tab-fill: var(--neutral-bg-alpha);
  --tab-fill-strong: var(--neutral-bg-component-soft);
  --tab-bar: var(--brand-border-strong);
}

.tab-item {
  /* ZERO VERTICAL PADDING, was `0.75rem 0`. The fill lives on .tab-link inside
     this element, so 12px here floated it clear of the rule instead of letting
     it reach down to it -- a geometry consequence of the design, not a spacing
     decision. */
  padding: 0;
  text-decoration: none;
  /* Pulls the item over the strip's own border so the bar below lands ON the
     rule: 2px of bar, the lower 1px covering the rule and 1px standing proud,
     which is what both mockups measure. */
  margin-block-end: -1px;
}

.tab-link {
  /* An <a>, so it needs to be a box before any of the padding or the fill can
     render as one. */
  display: block;
  /* FLUID, AND A DELIBERATE EXCEPTION TO THE FLAT-INSIDES RULE -- Joy's call,
     2026-10-29, asked for directly: *"Can we set our responsive padding on the
     tabs? Also can we decrease the block padding in relation to the inline
     padding a bit more?"* It was raised with her that this cuts against the
     rule she set after the callout episode -- a component's insides stay flat
     while only page flow rides the fluid --space-* scale -- and she took the
     exception knowingly. The reasoning that makes it an exception rather than a
     reversal: a tab strip spans the content column and reads as page furniture,
     where a callout's insides do not. The rule still stands everywhere else,
     including the panel below, which is why .tab-inner was left alone.

     WHAT THIS REPLACED, kept because it is the argument to re-read before
     touching these values. It was Codex's trigger padding 1:1 -- its rule is
     `padding-block: calc(var(--spacing) * 3.5)` and
     `padding-inline: calc(var(--spacing) * 4)`, --spacing being Tailwind's 4px
     base, so a flat 14px / 16px. Flat was right for matching Codex, because
     Codex's scale IS flat Tailwind: these two tokens agree with it only at
     their floors and then drift. So this is further from the mockup at every
     width above the floor, and that is accepted rather than overlooked.

     THE RATIO IS THE POINT, not either value on its own. 1:2 at every width
     (8/16 narrow, 12/24 wide) against the 0.875 it was, which is the "decrease
     the block in relation to the inline" she asked for, and --space-2xs is the
     only step that is tighter than the old 14px at BOTH ends of the clamp --
     --space-xs would have grown to 18px on desktop, taller than what she was
     looking at when she asked for less. Chosen from measured options with the
     tab heights quoted: 30px narrow to 38px wide, against a flat 42px before.

     TARGET SIZE WAS CHECKED and is not a constraint here. The label is
     --step-0 at `line-height: 1`, so the box is text + padding and nothing
     else: ~30px at phone widths. That clears WCAG 2.5.8 Target Size (Minimum),
     24px, which is the AA requirement. It misses 2.5.5 Enhanced at 44px -- but
     so did the flat 42px it replaced, so no AAA target was given up. Tab width
     is label-driven and far past 24px. If block padding is ever cut again,
     24px of height is the floor to measure against, not 44px. */
  padding-block: var(--space-2xs);
  padding-inline: var(--space-s);
  /* The label's own leading was adding invisible height on top of the 14px
     above and below, making the tab read far taller than Codex's. At 1 the box
     is exactly text + padding, so the 14px is the whole story. Codex sets the
     same `line-height: 1` on its trigger. */
  line-height: 1;
  text-decoration: none;
  color: var(--neutral-text-soft);
  background-color: transparent;
  /* --radius-md, NOT the --radius-sm I first wrote. I had measured ~8px off the
     mockup corners and picked the nearer token; the DevTools rule names
     --radius-md outright, and the radius comment at css-brand-tokens.css:753
     records that our --radius-md is Codex's own token confirmed by name AND
     value from Figma Dev Mode. The named token beats my pixel read of a 2px-tall
     antialiased arc. */
  border-top-left-radius: var(--radius-md);
  border-top-right-radius: var(--radius-md);
  /* Transparent at rest rather than absent, so every tab is the same height and
     nothing shifts when one becomes active. */
  border-block-end: var(--border-width-heavy) solid transparent;
  /* THIS DURATION IS ALSO THE TAB-SWITCH DELAY, which is the opposite of
     obvious and is why it is not --transition-duration. window.Tab switches the
     strip by calling `Util.onTransitionEnd(oldLink, ...)` and then removing
     `is-shown` from that link -- but a link never carries `is-shown`, so no
     style changes, no transitionend is ever emitted, and the callback runs only
     on the helper's setTimeout fallback. That fallback's delay is
     `getTransitionDuration(oldLink)` (util.js:584), i.e. THIS value. So
     whatever is written here is dead time between the click and `is-active`
     moving at all -- nothing in the strip changes until it elapses.

     At --transition-duration, 300ms, Joy read the result as sluggish on
     2026-10-29: the click was followed by 300ms of nothing, then the fill faded
     in over a further 300ms, while the panel had already finished swapping at
     200ms. --motion-duration-control is the token named for a control's own
     state change and fixes all three of those at once:
       - the dead time halves to 200ms;
       - the strip and the panel now land together, because `.fade` is on this
         same token;
       - reduced motion is finally honoured. --transition-duration is the one
         duration in the theme NOT zeroed in the reduced-motion block
         (css-brand-tokens.css:1289 covers the --motion-* family and
         --default-transition-duration, not this legacy token), so until now a
         reduced-motion reader paid the 300ms stall as well. At 0s the fallback
         fires next tick and the switch is instant, which is safe for exactly
         the reason recorded at `.fade` below.

     The sitewide --transition-duration is deliberately left alone: 31 uses
     across eight files, most of them on pages already signed off.

     IF THIS STILL READS SLOW, the next step down is --motion-duration-instant
     (75ms) -- but note that shortens the visible fill fade too, since one
     value is doing both jobs. Decoupling them is possible and was considered
     and rejected: util.js parseFloats only the FIRST duration in the list, so
     a 0s property listed first would release the widget immediately while the
     others still animated. That works and is far too fragile to rely on -- it
     would silently undo itself the moment someone reordered this list. */
  transition:
    background-color var(--motion-duration-control),
    border-block-end-color var(--motion-duration-control),
    color var(--motion-duration-control);

  /* EXPLICIT `&` THROUGHOUT. The two nested blocks this replaced had none, so
     they resolved as `.tab-item *:hover` and `.tab-item .is-active` --
     descendant selectors that happened to hit because .tab-link is a child.
     See the CSS-nesting note in the project memory; `is-active` is set by the
     widget on the LINK, not on the item. */
  /* `:not(.is-active)` RATHER THAN RELYING ON SOURCE ORDER. Codex gives a
     hovered inactive tab its OWN underline -- .kb-tab-trigger:hover sets
     `border-color: var(--border-color-neutral)`, our --neutral-border -- so the
     hover is a fill AND a darkened rule segment, not a fill alone. Without the
     :not(), `.tab-link:hover` and `.tab-link.is-active` tie at (0,2,0) and only
     the later block wins the border; excluding the active tab states the
     intent instead of leaving the bar's survival to ordering.

     NO FILL ON THIS STATE, per Joy: moving onto another tab changes that tab's
     bar and nothing else. So the only background in the whole strip belongs to
     the ACTIVE tab, and --tab-fill below is now used by .is-active alone. The
     label still lifts to --neutral-text because Codex's own rule carries a
     `color: var(--tabs-fg-hover)` alongside the border; "don't highlight the
     background" is about the fill specifically.

     With the fill gone this grey is measured against the bare card, where it is
     2.01:1 light / 1.86:1 dark -- under 1.4.11's 3:1, deliberately. The bar is
     not doing identification work here: the ACTIVE tab is identified by its
     fill plus the indicator, and this is a pointer affordance on a tab that is
     not selected. What it has to do is read as a CHANGE to the baseline rule it
     replaces, and it does -- ΔE00 7.60 light / 7.34 dark from
     --neutral-border-soft, near-identical steps, which is why this is the one
     colour in the block needing no dark override. The loud alternative
     (--neutral-border-strong, ΔE00 44.0 from the rule) would pass 3:1 and then
     compete with the indicator for "this one is selected". */
  &:hover:not(.is-active) {
    color: var(--neutral-text);
    border-block-end-color: var(--neutral-border);
  }

  /* NO BAR HERE ANY MORE. The active tab's underline used to be this element's
     own border-block-end-color; it is now the single sliding .nav-tabs::after
     below, because a per-tab border cannot travel between tabs. The transparent
     border above stays -- it still reserves the 2px so nothing shifts, and it is
     still what the hover grey colours in. */
  &.is-active {
    background-color: var(--tab-fill);
    color: var(--neutral-text);
    font-weight: var(--brand-font-weight-semibold);

    &:hover {
      background-color: var(--tab-fill-strong);
    }
  }
}

/* THE SLIDING INDICATOR, Codex's .kb-tab-indicator rebuilt on ::after so it
   needs no extra markup -- and ::after does not become a flex item here because
   absolute positioning takes it out of flow.

   Codex's own declarations, for the record: position absolute, left 0, bottom 0,
   height 2px, `width: var(--reka-tabs-indicator-size)`,
   `translate: var(--reka-tabs-indicator-position) 1px`,
   `background-color: var(--brand-border)`, `transition-duration: .5s`. The two
   --reka-* values are set as inline styles by Reka UI's JS. Ours are set the
   same way, as --tab-indicator-* on this element, by assets/tab-indicator.js.

   WE NEED A Y OFFSET AND CODEX DOES NOT, which is the one structural difference.
   This strip is `flex-wrap: wrap`, so tabs can sit on more than one row, and a
   bar anchored to `bottom: 0` would detach from any tab not on the last row.
   Anchoring at `top: 0` and translating by a measured Y instead keeps it on the
   active tab's own bottom edge wherever that tab is. The script measures
   offsetTop + offsetHeight - 2px, which lands the bar exactly where the old
   border-block-end drew it, so the -1px on .tab-item is still what puts it on
   the rule -- Codex's hardcoded `1px` of Y does that job in its layout, and
   here it is already in the measurement.

   `transition: translate` RATHER THAN Codex's `all`: the width changes too, and
   `all` on an element that also inherits a background would animate the colour
   swap on a theme toggle. Naming the two travelling properties keeps the tier
   switch instant.

   --motion-duration-slow IS A KNOWN 0.1s DEVIATION. Codex inlines .5s; our
   nearest token is 0.6s, and its own note at css-brand-tokens.css:863 scopes it
   to "travel across an element's own width, not state acknowledgement", which is
   exactly this. Taking the token keeps reduced motion working -- the durations
   zero at the token layer (:1288) -- where a hardcoded .5s would not. If the
   0.1s reads long in preview, that is the thing to change. */
.nav-tabs::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: var(--tab-indicator-w, 0);
  height: var(--border-width-heavy);
  translate: var(--tab-indicator-x, 0) var(--tab-indicator-y, 0);
  background-color: var(--tab-bar);
  /* Nothing to show until assets/tab-indicator.js has measured a tab. The 0
     fallbacks above already give the bar no width, so this is belt and braces
     for the moment between first paint and first measurement -- and it is what
     makes the no-JavaScript case clean rather than a stray 0-width bar parked
     at the origin. */
  opacity: 0;
}

.nav-tabs.has-tab-indicator::after {
  opacity: 1;
}

/* THE TRANSITION IS GATED ON A SECOND CLASS, which the script adds on the first
   `tab:show` and never removes. The bar gets repositioned by more than clicks --
   the initial measurement, the webfont swap, a breakpoint reflow, tabs
   rewrapping onto another row -- and none of those should be watched travelling.
   Gating here means load is silent and only a real tab change slides. */
.nav-tabs.has-tab-indicator.is-tab-sliding::after {
  transition:
    translate var(--motion-duration-slow),
    width var(--motion-duration-slow);
}

.nav-tabs > li::before {
  display: none;
}

.nav-tabs .nav-item,
.nav-tabs .nav-link {
  z-index: 1;
  text-decoration: none;
  font-size: var(--step-1);
  display: flex;
  color: var(--clr-neutral-160);
  line-height: 1.2;
}

.nav-tabs .dropdown-menu {
  border-top: none;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

[tabindex='-1']:focus {
  outline: none !important;
}

.tab-panel {
  padding: 1rem;
  margin-block-end: 1rem;
  display: none;
  border-top-left-radius: 0;
}

.tab-panel :last-child {
  margin-block-end: 0;
}

.tab-panel.is-active {
  display: block;
  border-top: 1px solid var(--clr-neutral-40);
  border-left: 1px solid var(--clr-neutral-40);
  border-right: 1px solid var(--clr-neutral-40);
}

/* STILL AN OPACITY-ONLY CROSSFADE. I briefly had the panel sliding here too,
   on the reading that the motion Joy saw in Codex was the content; it is the
   indicator. Keeping the panel still means the two do not compete, so the
   transform is gone again.

   DURATION IS A TOKEN NOW, was a hardcoded 150ms, and that part is worth
   keeping: reduced motion is honoured at the token layer
   (css-brand-tokens.css:1288), which a hardcoded value cannot be. The zeroing
   is safe here because window.Tab waits on Util.onTransitionEnd, which backs
   its listener with a setTimeout(getTransitionDuration) fallback -- at 0s that
   fires next tick and the panel still switches, rather than stranding the
   widget waiting for a transitionend that never comes.

   THE PHASES ARE SEQUENTIAL, not a crossfade: window.Tab removes `is-shown`
   from the outgoing panel, waits for its transition, and only then adds
   `is-active`/`is-shown` to the incoming one. So the felt fade is double the
   token, ~400ms against the old 300ms, while the indicator travels for 600ms
   alongside it. If the panel feels slow next to the bar,
   --motion-duration-instant is the other end of the scale.

   `.fade` reads generic but is tabs-only: article_page.hbs:524 is its single
   use and no bundle adds the class. */
.fade {
  transition: opacity var(--motion-duration-control) ease;
}

.fade:not(.is-shown) {
  opacity: 0;
}

/* THE PANEL IS NOT A BOX. Both mockups show the content sitting in open space
   under the strip -- no border, no fill, no radius -- so the 1px
   --clr-neutral-40 frame and the two bottom radii are gone from here and the
   --clr-white fill and radius are gone from .tab-inner below. The template also
   carried a `bg-white` utility on this element, the one class in its list that
   resolved (css-utilities.css:416); removing it there is part of the same change,
   because it would otherwise keep painting the panel white in dark mode and the
   migration would do nothing. */
.tab {
  display: none;
  /* padding: 1rem; */
  margin-block-end: 1rem;
  list-style: none;
}

/* `display: none` ON THE INACTIVE PANEL IS LOAD-BEARING and must stay FOR NOW.
   §1d of the authoring contract publishes "Unlike accordions, tabs cannot be
   accessed using ctrl+f", writers are choosing accordions over tabs BECAUSE of
   it, and so this cannot take the accordion's ::details-content treatment --
   that was chosen precisely to keep closed content findable.

   AN EARLIER VERSION OF THIS COMMENT CALLED THE ASYMMETRY A FEATURE and said
   not to "fix" it without asking. Joy answered that ask on 2026-10-29: "The
   product team has always wanted tabs that are possible to search using ctrl f,
   but maybe we should put that on our nice to have list after we get this
   initial pass done." So it is a limitation writers route around, not a choice.
   The §1d sentence is still true and stays published; it just describes a
   constraint we mean to lift one day rather than one we picked.

   DO NOT LIFT IT HERE ALONE. Making the panel findable is a four-part change,
   not a declaration swap -- `hidden="until-found"` on the panel, a `beforematch`
   handler that drives window.Tab so the strip and the indicator follow the
   revealed panel, a re-check of the `.fade` crossfade (content-visibility keeps
   a box where `display: none` had none), and a per-browser preview before the
   contract can promise it. Full cost and the open browser-support question are
   written up as item 7 in docs/brand-refresh/nice-to-have.md. */
.tab.is-active {
  display: block;
}

.tab-inner {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  /* Spacing left exactly as found -- see the SPACING note in the section header,
     and note that .tab-link's padding exception on 2026-10-29 stops here
     deliberately: the flat-insides rule still governs the panel.

     THE LABEL/CONTENT ALIGNMENT IS OPEN, and the numbers moved on 2026-10-29 so
     the old note here is superseded (it quoted .tab-link at a flat 0.75rem,
     which was already stale at 1rem before it became a token). The mockups align
     the panel's first line with the active tab's LABEL. That means comparing
     .tab-link's inline padding against this one: --space-s (16px -> 24px)
     against --space-xs (12px -> 18px), so the panel's text now starts 4px
     inside the label at narrow widths and 6px inside it at wide ones -- it never
     aligns, where the flat 1rem at least met it at 16px mid-range. Nobody has
     asked for this, so it is recorded rather than fixed. The one-line fix if it
     is ever wanted is --space-s on both edges, which is how the accordion
     solved the identical problem; the cost is a wider panel inset. */
  gap: 1rem;
  padding: var(--space-xs);
}
.tab img:first-child {
  margin-top: 0;
}

.tab p:last-child {
  margin-block-end: 0;
}

.scaled {
  transition:
    opacity 500ms ease,
    transform 500ms ease;
  transform: scale(1);
}

.scaled:not(.is-shown) {
  opacity: 0;
  transform: scale(1.1);
}

/*!SECTION - Tabs */

/*SECTION - Accordion */
/* Structural and visual accordion styles only.
   All accordion content spacing is managed in css-page-article.css. */

.collapse:not(.is-visible) {
  display: none;
}

.collapse {
  pointer-events: none;
}

/* The bare `details { display: flex; overflow: hidden; flex-direction: column }`
   that was here came out on 2026-10-26. It was the last global <details> rule in
   the theme, and it reached all five of them -- the three article sidebar panels,
   the share menu and the accordions below -- from a file named for components.

   All three declarations are now made by each consumer directly:
   .sidebar-article-dropdown and .article-share in css-page-article.css, and the
   accordions by `.article-content details` in that same file, which outranks a
   bare selector anyway and declared all three already. The accordions are
   rendered by tmpl-toggles from a `js-toggles` class the author types into
   {{article.body}} (article_page.hbs:250), which is inside .article-content, so
   that scoped rule always reaches them.

   Do not reintroduce a bare `details` rule. Two of the three consumers are not
   accordions and one of them is not even in the article body. */

/* THE CARD IS THE GROUP NOW, NOT EACH ITEM -- Codex's accordion, 2026-10-28.
   Joy: "Here is what we are going to make", with the Codex docs site's accordion
   in both states. Before this, every item was its own 1px box and an open item
   inverted to a --clr-neutral-160 fill with white text. Now .toggles is a single
   rounded card and the items are rows inside it, divided by hairlines.

   THE PADDING IS FLUID AND THE REST OF THE SPACING IS FLAT, and both halves are
   deliberate. The general rule on this project is that a component's insides
   stay off the --space-* clamps -- Codex's spacing is Tailwind's flat scale, and
   only the page's flow rhythm rides ours. Joy took the accordion's padding out
   of that rule on 2026-10-28, in two asks: the panel equal on all four sides,
   then the summary's two edges. So:

     fluid   .article-accordion-label   --space-xs / --space-s
             .article-accordion-content-wrapper   --space-s all round
             .toggles' own flow margin (in css-page-article.css, and fluid for
             the original reason -- it is page spacing, not component spacing)

     flat    the label's `gap` to the chevron, the card's 1px border, the 1px
             divider between rows, --radius-md

   THE TWO PADDINGS SHARE --space-s ON PURPOSE and that is the one constraint to
   respect if you change either: it is what keeps panel copy flush with the
   title. The reasoning is at each rule.

   TYPE IS THE EXCEPTION AND ALWAYS HAS BEEN: sizes ride the --step-* scale, so
   the title below is --step-1 and the panel's ladder runs down from it. Do not
   read the flat-spacing rule as covering font-size.

   THE PADDING STARTED AS CODEX'S, ADDED UP, and both edges have since moved to
   our own scale -- worth recording because the Codex numbers are what the
   current tokens were chosen against. Their trigger's padding is split across
   two elements, a row at `py-1 px-1.5` wrapping a button at `py-2 px-3.5`, and
   our <summary> is both of those at once, so it carried the sum: 12px block,
   20px inline. The panel was their `pt-1.5 pr-11 pb-3 pl-5`. Neither is those
   numbers now; see each rule for what replaced it and why.

   TWO DELIBERATE DEPARTURES FROM CODEX, both Joy's, same day:

     1. An open row takes a fill. Codex's open row is transparent -- measured,
        not assumed: with the pointer away from the component every row's
        computed background-color is rgba(0,0,0,0) in both states, and the pale
        teal that shows on an open row in a screenshot is just hover. Joy asked
        for the hover colour to become the open state, so `open` and
        `closed + hover` share --accordion-row-fill and the chevron is what
        separates them.

     2. The chevron rotates. Codex swaps the glyph (their open chevron's computed
        transform is `none`), which cannot be animated. Joy: "I want our chevron
        to have the animation -- not do an icon swap."

   Departure 1 is why --accordion-row-fill-strong exists: with open and hover on
   one value, hovering an open row would otherwise do nothing. Joy: "maybe we can
   do a slight darkening on hover." */

.toggles {
  /* THE TWO INTERACTIVE FILLS ARE NAMED HERE AND NOWHERE ELSE, so the dark tier
     is one override instead of four, and so the contrast work below is attached
     to the values it was measured on.

     Light ink is --neutral-text #10171e throughout: 17.89:1 on the card,
     15.75:1 on the fill, 13.58:1 on the strong fill. Dark ink is #ebf0f4:
     12.73:1, 9.23:1, 14.35:1. All six clear AAA, which is the bar on this
     project -- AA is the requirement, AAA the target wherever it is reachable,
     and here it is reachable everywhere, so do not spend it.

     A CONTRAST RATIO CANNOT SEE A STATE CHANGE ON ONE SURFACE, so the steps are
     measured as ΔE00 against the state they replace: 4.42 card -> fill and 3.54
     fill -> strong in light, 7.12 and 10.12 in dark. The first draft of the
     strong fill was --neutral-bg-component-soft-active #deebec, the next step
     down the same soft ramp, and it was rejected at ΔE00 1.49 -- about the
     just-noticeable threshold for two patches side by side, and this is a change
     over time on one patch, so it would have read as nothing happening. */
  --accordion-row-fill: var(--neutral-bg-component-soft-hover);
  --accordion-row-fill-strong: var(--neutral-bg-component-hover);

  display: flex;
  flex-direction: column;
  background-color: var(--neutral-bg);
  border: 1px solid var(--neutral-border-soft);
  border-radius: var(--radius-md);
  /* Clips the rows' fills and the dividers to the rounded corners. It is also
     why the label keeps an inset focus ring rather than Codex's outline -- see
     the ring below. */
  overflow: hidden;
}

/* THE DARK TIER CANNOT REUSE THE LIGHT VALUES, and the reason is a collision
   rather than a preference. --neutral-bg-component-soft-hover is #2e3e4e in
   dark and so is --neutral-border-soft, so an open row's fill would be exactly
   the colour of the divider below it and of the card's own border: both edges
   disappear. That is the same fill-against-border failure that cost the article
   table its column lines (css-brand-tokens.css:433).

   Joy chose to move the fill rather than lighten the card's edges -- one surface
   in one tier, instead of changing how every closed card looks in dark.

   --neutral-bg-alpha IS #ffffff1a, a 10.2% white wash, so it composites to
   #35404a over the card: 9.23:1 ink, ΔE00 7.12 from the card and 3.04 from the
   border. It is the only surface in the theme that clears both AAA and the
   border in this tier -- the band available to a #ebf0f4 ink at 7:1 is roughly
   #172029 to #2e3e4e, and #2e3e4e is the border. Being an alpha rather than a
   named surface is the cost of that, and it is deliberate: do not "fix" it to a
   solid token without re-measuring against the divider.

   The strong fill goes DARKER here, not lighter, which is still the darkening
   Joy asked for and is also the higher-contrast direction in this tier. */
:root[data-theme='dark'] .toggles {
  --accordion-row-fill: var(--neutral-bg-alpha);
  --accordion-row-fill-strong: var(--neutral-bg-component-soft);
}

.article-accordion {
  margin-top: 0;
  /* The divider. Codex ships a .kb-accordion-divider element for this and does
     not use it -- there are none in their rendered page -- so this is their
     item's own border-bottom, zeroed on the last row so the card's border is the
     only line at the foot. */
  border-bottom: 1px solid var(--neutral-border-soft);
  /* What lets the panel below animate to `block-size: auto`; the full reasoning
     is at the ::details-content rule. It is declared HERE, on the element, and
     deliberately not on :root -- it is inherited, so the pseudo-element picks it
     up from its originating element, and keeping it off the root means we are
     not changing how every other `auto` on the site interpolates. */
  interpolate-size: allow-keywords;
}

.article-accordion:last-child {
  border-bottom-width: 0;
}

.article-accordion-label {
  display: flex;
  /* FLEX-START, NOT CENTRE, and it is load-bearing for a wrapped title: it keeps
     the chevron on the first line instead of floating it to the middle of a
     two-line block. The chevron's own `height: 1lh` below is the other half.
     This replaces an `align-items: center` that existed only because the label
     used to be a fixed `height: 4.5rem`. That height is gone -- it is what made
     the old focus border widen the box and jump the text, and a row should be as
     tall as its content plus its padding. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.625rem;
  /* FLUID, AND THE INLINE HALF IS DELIBERATELY THE SAME TOKEN AS THE PANEL'S
     (Joy, 2026-10-28). Was a flat `0.75rem 1.25rem` -- Codex's two nested
     paddings added up, 12px block and 20px inline.

       block   --space-xs   12px -> 18px   (starts at the old 12px, then grows)
       inline  --space-s    16px -> 24px   (crosses the old 20px at 880px)

     THE INLINE TOKEN IS LOAD-BEARING: .article-accordion-content-wrapper is
     var(--space-s) on all four sides, so matching it here is what keeps the
     panel's copy flush with the title above it at EVERY width. While this was
     flat 20px against a fluid panel the two edges only lined up at one
     viewport. If either value changes, change both or the alignment goes. */
  padding: var(--space-xs) var(--space-s);
  cursor: pointer;
  /* --font-family-primary is Codex's Sora token and carries no fallbacks of its
     own, hence the stack (same as css-page-header.css:1616). */
  font-family: var(--font-family-primary), system-ui, sans-serif;
  /* --step-2, and the reason it is a step token at all is that --font-size-base
     was a flat 16px (Joy, 2026-10-28) -- the only type left in this component
     off the responsive scale, which made the title the one thing in the card
     that did not resize with the viewport while every size in the panel below
     it did. It went to --step-1 first, matching the old 16px almost exactly,
     and then the whole ladder moved up a step on Joy's call. --step-2 is
     17.7188px -> 17.9972px.

     THE "EVERY NUMBER HERE IS FLAT" RULE AT THE TOP OF THIS BLOCK IS ABOUT
     SPACING, not type -- Codex's spacing is a flat Tailwind scale and a
     component's insides stay off our --space-* clamps, but size has always
     ridden the step scale and the panel's ladder is built from it. */
  font-size: var(--step-2);
  font-weight: var(--font-weight-strong);
  /* Codex sets `line-height: 1` on .component-strong-lg and then overrides it to
     1.5 with a `leading-[1.5]` utility on the row, so 1.5 is the shipped value.
     Take the shipped one -- a unitless 1 would collapse a wrapped title. */
  line-height: 1.5;
  color: var(--neutral-text);
  background-color: transparent;
  /* On the base rule, not inside the hover and open rules. A transition declared
     only in the state it transitions *to* animates one direction and snaps back
     the other, because the element has no transition left to run once the rule
     stops matching -- that is exactly why the sidebar chevron
     (css-page-article.css:1193) opens smoothly and snaps shut. */
  transition: background-color 200ms ease-out;

  &:hover {
    background-color: var(--accordion-row-fill);
  }

  /* THE RING HAS TO BE INSET, and the reason survived the restyle even though
     the border it used to fight is gone. The card above is `overflow: hidden`
     with no padding, and a <summary> is flush to the card's inner edge on the
     first and last rows, so an outline at any offset is clipped there. Codex can
     use `focus-visible:outline-offset-2` because their padding sits on a wrapper
     *inside* the clip; ours is on the focusable element itself.

     An inset box-shadow is painted within the padding box, so it reads as a 2px
     ring just inside the row and costs no layout. `outline: none` is required to
     suppress the sitewide *:focus-visible ring, which would otherwise draw as a
     single stray line across the row's clipped edge.

     The sidebar's summaries are deliberately NOT like this: .sidebar-article-
     dropdown has `padding: 1rem`, so a 2px ring at a 2px offset has room and
     never reaches a clip. An outline is the sitewide default and this inset
     shadow is the exception -- do not "make them consistent". */
  &:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--form-focus-ring);
  }
}

/* Codex's equivalent is `min-w-0 flex-1` on their header element, and both
   halves matter. `flex: 1` lets the title take the row's free space so the
   chevron is pinned to the end edge; `min-inline-size: 0` is what lets it wrap
   at all, since a flex item's automatic minimum size would otherwise hold it at
   its longest unbreakable run and push the chevron out of the card. */
.article-accordion-title {
  flex: 1;
  min-inline-size: 0;
}

details[open] > .article-accordion-label {
  background-color: var(--accordion-row-fill);
}

details[open] > .article-accordion-label:hover {
  background-color: var(--accordion-row-fill-strong);
}

/* The chevron's wrapper. `height: 1lh` is Codex's trick and it is worth keeping:
   one line-height tall, so centring inside it puts the glyph on the middle of
   the title's FIRST line and it stays there however many lines the title wraps
   to. `flex: none` stops a long title squeezing it. */
.article-accordion-label > span[aria-hidden='true'] {
  display: flex;
  align-items: center;
  height: 1lh;
  flex: none;
}

/* Targeting the <svg>, not the <i>: the Font Awesome kit is SVG+JS and replaces
   the <i> it is given, so by the time this matters there is no <i> left. Pinned
   to --step-2 rather than inherited for the same reason the sidebar chevron is
   pinned (css-page-article.css:1045) -- it should track the title it sits
   beside, not whatever the summary happens to compute to. KEEP THIS EQUAL TO
   THE TITLE: it has followed the title twice now, off --font-size-base and then
   up the ladder, because changing one without the other shrinks the glyph
   against its own label. */
.article-accordion-label > span[aria-hidden='true'] > svg {
  font-size: var(--step-2);
  transition: rotate 200ms ease-out;
}

details[open] > .article-accordion-label > span[aria-hidden='true'] > svg {
  rotate: 180deg;
}

/* THE PANEL ANIMATES BOTH WAYS WITHOUT JAVASCRIPT, and it does not use Codex's
   mechanism. Theirs is a pair of keyframes on `height`, from 0 to a
   --reka-accordion-content-height their framework measures and writes as an
   inline style; we have no such number and would have to add a script to get
   one.

   IT IS ALSO NOT THE TABLE OF CONTENTS' MECHANISM, and that is the measured
   part. The first version of this rule copied the `grid-template-rows: 0fr <->
   1fr` pair from css-page-article.css:1173 on the reasoning that an `fr` needs
   no measurement. It snapped, both ways. A four-variant probe on the live
   element (2026-10-28) recorded the panel's height every frame:

     0fr <-> 1fr        49 49 49 139            instant, both directions
     0px <-> 90px       49 61 72 83 ... 139     clean ramp
     block-size 0<>auto 49 81 93 103 ... 139    clean ramp

   So `grid-template-rows` is not the problem and neither is the duration --
   the `fr` UNIT IS. An `fr` is a share of the container's free space, and that
   only exists if the container's height is definite. This panel's is
   indefinite: it is content-sized in normal flow, so `0fr` and `1fr` BOTH
   resolve to max-content and there is no difference to interpolate. The first
   recorder said so before the probe did -- it read `grid-template-rows: 90px`
   in the CLOSED state as well as the open one.

   Which means the table of contents' copy of this works only because its
   column is height-constrained, and would snap in the same way whenever that
   column is roomy enough to leave it auto-height. Logged, not fixed here: it
   is a different component on a page we have signed off, and it should be
   looked at on its own. The note there also claims `interpolate-size` has
   support "not close to" `::details-content`'s, which is backwards --
   interpolate-size shipped FIRST, so anything that can match this selector can
   also run this transition.

   block-size: 0 <-> auto IS THE RIGHT ONE OF THE TWO THAT WORK. The px pair
   animates just as well but 90px is one accordion's content height and nothing
   else's, so it needs exactly the measurement we are trying to avoid. `auto`
   needs no number and is always exactly right, which also makes the open and
   close symmetrical -- a `max-height` cap (the sidebar cards' approach, :1133)
   was the fourth variant and is rejected for that reason: at a cap of 25rem
   against 90px of content it reached full height in a third of the duration
   and sat there, which is the abrupt open we started out trying to fix.

   content-visibility IS THE HALF THAT MAKES CLOSING WORK, and it was already
   working before the probe -- the recordings showed the close holding full
   height to ~190ms while nothing else moved. Removing `open` un-renders a
   <details>' children in the same frame, so without this the panel would jump
   from full height to gone with nothing to interpolate; the whole diagnosis is
   at css-page-article.css:1089. `allow-discrete` defers that flip to the END
   of the transition, which is what buys the collapse its 200ms.

   `overflow: hidden` IS NOT OPTIONAL. It clips the wrapper's padding, which
   would otherwise paint as a strip of dead space under a closed row.

   200ms ease-out is Codex's own duration, and the chevron above matches it
   rather than running at the sidebar's 200ms-against-400ms mismatch. */
.article-accordion::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size 200ms ease-out,
    content-visibility 200ms allow-discrete;
}

.article-accordion[open]::details-content {
  block-size: auto;
}

/* No fill and no borders any more. The card paints the surface and the item
   paints the divider, so this element is padding only.

   ITS --clr-white FILL LEAVING IS WHAT FORCED A CHANGE IN css-utilities.css:
   that file held `.article-accordion-content-wrapper { color: var(--clr-neutral-160) }`
   inside :root[data-theme='dark'], a dark ink that only ever made sense while
   the surface under it stayed stubbornly white. With the fill gone the panel
   inherits the card's --neutral-bg, which has a real dark tier, so holding
   #333333 there would have put dark ink on a #1e2a35 panel. The selector is
   removed from that list; the condition for doing so is spelled out in the
   comment above it. */
/* RESPONSIVE AND EQUAL ON ALL FOUR SIDES, 2026-10-28, and this one is Joy's
   explicit call against the flat-spacing rule at the top of this block -- she
   asked for it twice. Record of what it replaced and what that cost:

     was  0.375rem 2.75rem 0.75rem 1.25rem   6px / 44px / 12px / 20px
     now  var(--space-s)                     16px -> 24px, all four

   THE TOKEN WAS PICKED OFF THE 20px INLINE EDGE, not off an average of the
   four. That edge is the panel's geometry -- the other three were Codex's
   asymmetric `pt-1.5 pr-11 pb-3 pl-5`. 20px has no exact token; --space-xs tops
   out at 18px, which is the nearest value on the scale, so one step up from it
   is --space-s. Do not re-derive this from the 6px or 12px edges: matching a
   flat Codex number to the token whose FLOOR happens to equal it is the mistake
   that scale invites.

   TWO THINGS THIS GIVES UP, both deliberate, both visible in preview:

     1. The 44px end edge is gone, so panel copy now runs as far right as the
        card allows instead of stopping short of the chevron's column. The
        chevron is in the summary row, not here, so nothing collides -- it was
        an optical reservation, and equal padding cannot keep it.

     2. RESOLVED, and recorded because it is the reason the summary's inline
        padding is the token it is. For one round this was --space-s against a
        flat 20px summary, so the panel's start edge only lined up with the
        title at a 880px viewport -- tighter below it, looser above it.
        Joy then put the summary on var(--space-xs) var(--space-s), so both
        inline edges are now the same token and flush at every width. KEEP THEM
        EQUAL: this rule and .article-accordion-label's inline value are a pair,
        and changing one alone brings the misalignment back. */
.article-accordion-content-wrapper {
  padding: var(--space-s);
}

/*!SECTION - Accordion */

/* SECTION - Tags */
[class*='tag-'] {
  color: var(--fg, red);
  background-color: var(--bg, purple);
  font-size: var(--step-0);
  font-weight: var(--font-bold);
  border: 1px solid var(--accent, red);
  border-radius: 50px;
  padding: 0.25rem 0.75rem;
  margin-top: 1rem;
}

.tag-default {
  --fg: var(--clr-neutral-160);
  --bg: var(--clr-neutral-5);
  --accent: var(--clr-neutral-40);
}

.tag-default--dark {
  --fg: var(--clr-neutral-5);
  --bg: var(--clr-neutral-160);
  --accent: var(--clr-neutral-160);
}

.tag-success {
  --fg: var(--clr-success-140);
  --bg: var(--clr-success-10);
  --accent: var(--clr-success-140);
}

.tag-success--dark {
  --fg: var(--clr-success-5);
  --bg: var(--clr-success-140);
  --accent: var(--clr-success-60);
}

.tag-alert {
  --fg: var(--clr-alert-140);
  --bg: var(--clr-alert-10);
  --accent: var(--clr-alert-140);
}
.tag-alert--dark {
  --fg: var(--clr-alert-5);
  --bg: var(--clr-alert-140);
  --accent: var(--clr-alert-60);
}

.tag-secondary {
  --fg: var(--clr-secondary-140);
  --bg: var(--clr-secondary-10);
  --accent: var(--clr-secondary-140);
}
.tag-secondary--dark {
  --fg: var(--clr-secondary-5);
  --bg: var(--clr-secondary-140);
  --accent: var(--clr-secondary-60);
}

.tag-none {
  display: none;
  margin-top: 0;
}
/*!SECTION - Tags*/

/* SECTION - Badges */
/* Codex Badge at soft emphasis, measured 1:1: 1px border, --radius-sm,
   4px/6px padding, 12px SemiBold on a 22px box. Codex has no size prop, so
   there is exactly one badge size.

   The fallback is now neutral, not the old red-on-purple. That was reachable
   for real: Guide also emits `new` and `closed`, and neither had a class here.

   The four `--dark` variants that used to sit here are gone. Nothing in any
   template or script ever appended a `--dark` class, and dark mode is driven
   by `:root[data-theme='dark']`, so they had never once rendered — one of them
   was misspelled `.tag-solved--dark` besides. The role tokens below carry both
   tiers themselves. */
[class*='badge-'] {
  display: inline-block;
  color: var(--fg, var(--neutral-text));
  background-color: var(--bg, var(--neutral-bg-soft));
  border: 1px solid var(--accent, var(--neutral-border));
  border-radius: var(--radius-sm);
  padding: 0.25rem 0.375rem;
  font-size: var(--step--1);
  font-weight: var(--font-bold);
  line-height: 1;
  white-space: nowrap;
}

.badge-default,
.badge-closed {
  --fg: var(--neutral-text);
  --bg: var(--neutral-bg-soft);
  --accent: var(--neutral-border);
}

/* Guide shows `new` to end users as "Open", so both take the same chip. */
.badge-new,
.badge-open {
  --fg: var(--info-text);
  --bg: var(--info-bg);
  --accent: var(--info-border);
}

/* "Awaiting your reply" — the one status that needs the requester to act. */
.badge-answered {
  --fg: var(--warning-text);
  --bg: var(--warning-bg);
  --accent: var(--warning-border);
}

.badge-solved {
  --fg: var(--success-text);
  --bg: var(--success-bg);
  --accent: var(--success-border);
}
/*!SECTION - Badges */

/*SECTION - Callouts */
/* Codex Message, measured 2026-10-25 from the Component Library's Message page
   (Documentation frame 22121:6726, Components frame 6872:72533). A severity is
   carried by four role tokens -- surface, border, icon, ink -- so the whole
   conversion is the four custom properties each variant sets below.

   THE CLASS NAMES ARE A FIXED API. Article authors have pasted `callout-*` into
   content we cannot edit, so renaming them to Codex's severity names is off the
   table however badly the two vocabularies line up. Joy's mapping, 2026-10-25,
   hue-preserving wherever a name disagrees with Codex's semantics:

     note, support  -> info      (was legacy plum)
     tip            -> success
     remember       -> high      (was brand vermillion; Codex has no
                                  brand-tinted message at all -- brand exists
                                  only as a solid fill -- and `high` is the
                                  nearest hue)
     warn, warning  -> critical  (was legacy red, and stays red so every
                                  already-published article still reads the way
                                  it was written; Codex does ship a separate
                                  amber `warning`, and reserves `critical` for
                                  destructive actions, so the name is a knowing
                                  compromise)
     banner         -> success

   The icon is its own role and is BRIGHTER than the ink -- critical is #e52124
   on #9d070a, not one colour for both. Ours had no icon colour at all, so icons
   inherited the ink; `--icon` below is what fixes that.

   THE ICON ITSELF NOW ARRIVES FROM JAVASCRIPT, assets/callout-icons.js, and the
   six `::before` rules that used to carry it were deleted here on 2026-10-25.
   They were dead: `font: var(--fa-font-regular)` depends on a variable this
   theme never defines, so the codepoint rendered tofu, and all six had
   `display: none` on them as a result. That is a theme-wide problem rather than
   a callout one -- see the ICONS note at style.css:375, and item 6 in
   docs/brand-refresh/nice-to-have.md for the root-cause fix -- so the icon is
   an <i>, which is the one mechanism that works here, keyed off the same class
   mapping as before. Icon per SEVERITY, not per class: circle-info for both
   info classes, circle-check for both success, octagon-exclamation for high,
   triangle-exclamation for critical, all solid (Joy's kit, 2026-10-25).
   `--icon` stays a fallback rather than a required part, because the script
   leaves an authored icon alone where markup already has one -- and because
   nothing except the script guarantees one.

   THE RULES IMMEDIATELY BELOW ARE LIGHT MODE ONLY. Dark mode uses Codex's
   strong emphasis instead, in its own block at the end of this section -- so
   the four properties each variant sets here are overridden wholesale there,
   and a change to one tier is not a change to both.

   Both tiers of the REGULAR emphasis were read, and the warning is kept because
   the regular dark values still feed the chip and icon in the strong block: do
   NOT take ink values from the frame's dark panel. It paints neutral, critical,
   success and info text pure `#ffffff`, which is the same Figma-versus-shipped
   disagreement settled on 2026-10-24 -- Codex's live dark stylesheet never puts
   pure white on text, and our dark tier already carries the corrected per-hue
   values. The role references throughout are what make that automatic.

   Geometry matches Codex as of 2026-10-25 as well: the radius is 6px, which is
   `--radius-sm` exactly, and the icon-to-text gap is 20px. Two rules already
   overrode the old 8px gap to 1.25rem by hand -- `.callout-support` here and
   `.callout-tip` in css-page-new-request.css -- so this moves the value to the
   base and drops both copies; the `align-items: center` each of them also sets
   is theirs and stays. The gap is sitewide for now and may come back when
   article callouts are looked at in their own right (Joy, 2026-10-25). Padding
   needs nothing: Codex is ~24px, ours is 1.5rem. Title and description share
   one ink and differ only in weight, which is already how ours behave. */
[class*='callout-'] {
  color: var(--fg, var(--neutral-text));
  background-color: var(--bg, var(--neutral-bg-component));
  font-size: var(--step-1);
  border: 1px solid var(--accent, var(--neutral-border));
  border-radius: var(--radius-sm);
  padding: 1.5rem 2.5rem 1.5rem 1.5rem;

  /* A CALLOUT WITH MORE THAN ONE BLOCK IN IT LAID OUT AS COLUMNS UNTIL
     2026-10-28. This was `display: flex` with no `flex-direction`, so it
     defaulted to `row`: correct for the shape the component was built for --
     icon beside one block -- and wrong for anything else. Joy's repro was a tip
     callout holding a div, a <p> and an <ol>, which rendered as four
     side-by-side columns with the list stranded at the right-hand edge.

     THE AUTHORING CONTRACT DOES NOT SAVE US HERE, which is the part worth
     knowing. article-authoring-contract.md section 3 documents callouts as
     "always this shape" -- one nested plain <div> wrapping the content -- and
     that is genuinely all a single-block callout needs. But the markup in the
     repro carries CKEditor's own `data-list-item-id` attributes, so it is
     editor output, not hand-typing: pressing Enter in the Zendesk WYSIWYG
     emits a SIBLING <p>, and building a list emits a sibling <ol>. No keystroke
     nests a second block inside that first div. So the contract's shape is
     unreachable for multi-block content and the component has to accept loose
     siblings. Do not "fix" this by tightening the contract -- writers cannot
     comply with it, and article bodies are published content we cannot edit.

     GRID RATHER THAN `flex-direction: column`, because column would stack the
     icon ON TOP of the text instead of beside it. Two tracks: the injected icon
     owns column 1, everything else stacks down column 2, and the row count
     follows the content with no wrapper element needed. A wrapper is what the
     contract asks for and it cannot be relied on, per above.

     WHY THIS IS SAFE FOR THE PAGES ALREADY SIGNED OFF. Every template-side
     callout has exactly one content child -- new_request_page.hbs:161 and :166
     -- and a one-child grid and a one-child row render identically. The nested
     cases opt themselves out: callouts inside a list (css-page-article.css, the
     Lists block) and inside a table cell (ditto, Callouts in tables) both set
     `display: inline-flex`, which out-ranks this declaration, so they keep the
     row they want and keep hiding their icon with no empty track left behind.

     THE GAP HAD TO SPLIT IN TWO. It was a single `gap: 1.25rem`, which in grid
     would govern the rows as well. 1.25rem is Codex's icon-to-text measurement
     and belongs to the column only. Nothing else spaces these children -- the
     `.article-content > * + *` rhythm is a DIRECT-child rule and stops at the
     callout itself -- so the row value is the component's to set and there is
     nothing underneath it to double.

     THE ROW VALUE IS CODEX'S OWN, from Joy's DevTools dump on 2026-10-28, and
     not a number chosen for the article body. Codex stacks its Message content
     in `flex flex-col gap-1` -- `calc(var(--spacing) * 1)`, a flat 4px -- so
     0.25rem is that declaration, translated. The 8px you can measure between
     the title and the first bullet in the reference screenshot is NOT this gap
     on its own: it is `gap-1` plus the `mt-1` Codex puts on the list, and the
     list half of that lives with the list, in css-page-article.css's Lists
     block, so that a list gets it and a second paragraph does not.

     FLAT, NOT `--space-3xs`, even though that token's floor is this value.
     --space-3xs is a fluid clamp (0.25rem -> 0.375rem) and Codex's spacing
     scale is flat, so the token would match the reference on a narrow viewport
     and sit half again above it on a wide one. The rest of this rule is flat
     for the same reason. Logged as a value-right/name-wrong deviation in
     plan.md, Phase 1.

     THIS IS DELIBERATELY TIGHT FOR PROSE and is an experiment Joy asked for in
     those terms -- "let's start with Codex for everything and see where that
     takes us" -- after an earlier pass of this fix used 1.5rem, reasoning from
     the article body's block rhythm instead of from the component Codex ships.
     Worth knowing which way to move it if it reads too tight: Codex's Message
     holds a short title and terse bullets, where an article callout can hold
     two full paragraphs, and 4px between those is much less air than 4px
     between a title and a list. The reference cannot answer that case because
     it does not contain it. */
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 1.25rem;
  row-gap: 0.25rem;

  /* `> svg:first-child` and not the bare `svg` the size rule below uses: that
     one is deliberately a descendant selector so inline author icons take the
     severity colour, and only the FIRST CHILD is the icon callout-icons.js
     injects. The same distinction is already drawn in the two nested blocks in
     css-page-article.css, which narrowed to `> svg:first-child` on 2026-10-26
     after the unqualified version hid authors' inline icons too.

     A callout whose class is not in callout-icons.js's mapping gets no icon at
     all and so leaves column 1 empty -- the track collapses to 0 but the
     column gap still draws, indenting the content 1.25rem. No such class ships
     (`.callout-banner` is the only unrendered one and it is in the mapping), so
     this is noted rather than guarded against with a `:has()`. */
  > svg:first-child {
    grid-area: 1 / 1;
  }
  > *:not(svg:first-child) {
    grid-column: 2;
  }

  /* --step-5 is the icon size for every callout, which until 2026-10-25 was
     true of three of them and not of the ones in articles. Those inherited the
     callout's own --step-1 and drew at ~16px, against ~25px everywhere else --
     Joy: "Since the size is set to 1em, they are very small."

     --step-5 rather than a new value for two reasons. It is already the size on
     `.callout-support`, `.callout-banner` and `.callout-tip`, all three of
     which Joy has previewed and approved, so this adopts a signed-off number
     instead of inventing a fourth one. And it lands on Codex, whose Message
     icon is 24px: --step-5 clamps to 25.2-25.6px and --step-4 to 22.4-22.8px,
     which are equally far from 24 in opposite directions, so the tie goes to
     the one already on the page.

     The 0.25rem top nudge that used to be here is gone with it. That nudge
     centred a 1em glyph against its line of text; at --step-5 the icon is
     about the height of a --step-1 line box, so the two now align at the top
     on their own -- which is why all three of those overrides were zeroing it
     anyway. Nothing sets a top margin on this icon now. */
  svg {
    color: var(--icon, var(--neutral-icon));
    font-size: var(--step-5);
    margin-right: 0;
  }
  strong:first-child {
    font-weight: var(--font-bolder);
    margin-right: 0.25rem;

    /* THE MARGIN ABOVE IS WHY A CALLOUT IN A TABLE CELL OVERFLOWED IT, and this
       zero-width space is the fix. Measured on Joy's repro, 2026-10-28, viewport
       1875px: a `.callout-remember` in a five-column table had 139.6px of cell
       content box and drew 162px wide, escaping by ~22px.

       Authors write the label with no space after it -- `<strong>Remember:</strong>Replace
       this text...`, and the same in the tip example in the authoring contract.
       The gap you see is not a space, it is `margin-right`, which is this rule
       standing in for the one the author left out. BUT A MARGIN IS NOT A BREAK
       OPPORTUNITY. With nothing breakable between them, `Remember:Replace` is a
       single 138px token, and that token is the content's min-content width.

       An inline-flex box in a table cell is shrink-to-fit, and shrink-to-fit's
       floor is min-content -- it overflows its container rather than go under
       it -- while `table-layout: fixed` (css-reset.css:498) means the cell can
       never widen to accommodate content. 138px of unbreakable text plus this
       component's 12px of padding each side is the 162px that escaped.

       U+200B restores the break opportunity the missing space would have given,
       at zero width, so min-content drops to the widest real word (~75px here).
       Shrink-to-fit then clamps to the cell instead of to the token and the
       overflow is gone without capping anything.

       WHY NOT `max-inline-size: 100%`, which is the reflex fix: it caps the
       BOX, not the text. The 138px token would then overflow the capped box
       instead of the cell, so the words would sit outside the coloured panel --
       the same bug moved one element in. And `overflow-wrap: break-word` does
       not help either, because it does not change min-content; `anywhere` does,
       but it buys the shrink by breaking mid-word, so a tight cell would render
       "Remembe / r:Replace". Giving the text a correct break opportunity is
       cheaper than both and is the thing that is actually missing.

       NOTHING MOVES WHERE THERE IS ROOM, which is why this sits on the base rule
       rather than in the table block: a break opportunity only changes layout
       when the line has to break, so callouts in the article body -- where the
       content column is `1fr` and min-content is not the constraint -- render
       identically. It also means a callout authored WITH the space is unaffected;
       it already had a break here and gains a second one of zero width. */
    &::after {
      content: '\200b';
    }
  }
  /* Links take the severity's ink, so they read as part of the message rather
     than as the sitewide link colour. The background tint these used to get on
     hover is gone -- it was a local copy of the one deleted sitewide on
     2026-10-24, and Codex has no bg-hover role inside a severity to point it
     at. Hover is now the sitewide colour change, which reaches these at a
     higher specificity than this rule. */
  a {
    color: var(--fg, var(--neutral-text));
    align-items: flex-start;
    /* `text-decoration-thickness` and `text-shadow` are listed because the
       hover below moves those two instead of the colour, and both are safe to
       animate -- neither changes a metric, so there is no reflow to ease. The
       `font-weight` that briefly stood in for the shadow was deliberately left
       OUT of this list for exactly that reason; see the note on the hover rule.

       `color` stays listed even though the ink no longer changes inside a
       callout: `{{dc}}` content and the authoring contract can both put a class
       on an anchor here, and the sitewide `.link:hover` is still a colour change
       that this rule does not out-rank in every arrangement. The duration is the
       sitewide control token, so a callout link and a prose link move at the
       same speed. */
    transition:
      color var(--motion-duration-control) var(--ease-in-out),
      text-decoration-thickness var(--motion-duration-control)
        var(--ease-in-out),
      text-shadow var(--motion-duration-control) var(--ease-in-out);
  }

  /* The hover has to be restated rather than inherited, for the same reason
     four other places restate it: a class is what stops `a:not([class])`
     reaching an anchor. Unclassed links in a callout already behaved this way
     -- `a:not([class]):hover` outranks the rest-state rule above -- so this
     only extends it to anchors that carry a class, which `{{dc}}` content can.

     THE INK NO LONGER MOVES IN A CALLOUT, 2026-10-28. The paragraph that used
     to sit here said the opposite -- that the sitewide --brand-text was right
     here and "inventing a per-severity one would be a second convention for the
     same gesture" -- and it was wrong on a measurement it never took. Joy: "the
     hover color in callouts is probably not accessible, and in some of the
     callouts you cannot see any difference at all on hover."

     `.callout-warn` is the one she saw. --critical-text is --color-cherry-12 and
     --brand-text is --color-vermillion-12, the SAME STEP INDEX on adjacent red
     hues, so #9d070a hovered to #992008 is dE00 3.2 with dL* +1.0. A CONTRAST
     RATIO CANNOT DETECT THIS, which is the method lesson: both inks pass AAA on
     the fill (7.38:1 and 7.10:1) and the defect is rest-ink-against-HOVER-ink.
     For any state change that keeps the same surface, dE00 against the rest
     state is the test, not a ratio against the background.

     THEN THE OBVIOUS FIX FAILED TOO, and this is why the mechanism changed
     rather than the value. Each variant got a hover two ramp entries darker on
     its own hue -- dE00 11.7 / 16.9 / 14.5 / 16.4, all AAA on their fills. Joy,
     on preview: "these steps are almost too subtle to notice." She is right and
     the numbers oversold it: the old hover worked on three callouts by moving
     HUE, while a step move only moves LIGHTNESS, and there is almost no
     lightness to move. AAA on these pale fills caps the ink at L* 34.8 and the
     rest inks are ALREADY at L* 26.0-40.2, so a hover can only go darker, which
     reads as "slightly darker text" and nothing else.

     THE COLOUR AXIS IS EXHAUSTED IN BOTH DIRECTIONS AT BOTH FLOORS, measured
     over all 214 primitives rather than argued:
       - darker, same hue: invisible, as above.
       - lighter, same hue, AAA: nothing qualifies anywhere.
       - lighter, same hue, dropped to AA 4.5:1: dE00 6.3-13.9, i.e. no better
         than the darkening -- and `tip` has NOTHING, its rest ink is already the
         lightest step of lime clearing 4.5:1 on its own fill.
       - visible, AAA: only by jumping hue, and the winners are absurd --
         remember to indigo purple (dE00 42.2), warn to lime green (47.4). A
         warning link turning green reads as success. Declined by Joy.
     Codex offers no way out either: there is no `*-text-hover` and no
     `*-text-strong` for ANY severity, only `--*-text`, `--*-icon` and the
     `--*-bg-strong/-hover/-active` fills; the only `*-text-*` roles in the whole
     token file are --neutral-text-inverse and --neutral-text-soft.

     SO THE UNDERLINE CARRIES IT, which is Codex's own Hyperlink hover and not an
     invention. The measurement is at css-reset.css:417: default 0.97px, hover
     1.53px, active 1.60px, with the INK UNCHANGED across all three. That
     mechanism was removed sitewide on 2026-10-25 when Joy replaced it with a
     colour change, and the note there is careful to say "the value was right and
     it was the DESIGN that got overruled, not the reading". It comes back HERE
     only, on the one surface where colour has no room, and it is the only
     mechanism that fixes dark as well -- see the dark block, where no hover ink
     exists at all. Joy's call, 2026-10-28, off a three-way.

     THE RATIO IS CALC'D OFF --link-underline RATHER THAN HARDCODED so the 1.5x
     survives any change to the rest weight; that token is the handle
     css-reset.css:339 keeps for exactly this. At the rest weight's
     `max(0.08em, 1px)` this lands on Codex's own pair at --step-1 body size.

     --fg-hover IS WHY THE INK HOLDS STILL, and it is one declaration on the base
     rule rather than five values. `.article-content a:not([class]):hover` in
     css-page-article.css is (0,3,1) and out-ranks this rule at (0,2,1), so a
     bare <a> in a callout takes ITS hover, not this one -- and that file loads
     later (document_head.hbs:227 vs 220), so matching specificity would not help
     either. Rather than fight it, both rules read the same slot: setting it to
     this callout's own --fg makes "hover" resolve to the rest ink wherever the
     contest is won, so the ink cannot move no matter which selector lands it.
     The same trick `color: inherit` plays for the rest state. */
  a:hover,
  a:active {
    /* RESTATING THE REST INK IS NOT REDUNDANT, and dropping it was a real bug
       for the few minutes it lasted. The rest rule above is only (0,1,1), while
       css-reset.css:468's `a:not([class]):hover` is (0,2,1) and beats it on
       specificity no matter which file loads later -- so with no `color` here,
       an unclassed <a> in a callout would still jump to --brand-text. Article
       bodies happened to be covered, because the (0,3,1) rule in
       css-page-article.css reads the slot below, but callouts are not only in
       article bodies: new_request_page renders `.callout-support`, and `{{dc}}`
       content can put one anywhere. This declaration is what makes "the ink
       does not move" true on every page rather than one. */
    color: var(--fg, var(--neutral-text));

    /* Read by `.article-content a:not([class]):hover`, which out-ranks this
       rule at (0,3,1) and so decides the colour inside article bodies whatever
       is declared here. Pointing the slot back at this callout's own --fg makes
       that rule paint the rest ink too. See the note above. */
    --fg-hover: var(--fg, var(--neutral-text));

    /* 2x AND NOT CODEX'S 1.5x, which is a deliberate departure from the
       measurement quoted above and the reason is arithmetic rather than taste.
       --link-underline is `max(0.08em, 1px)` and callout text is --step-1, which
       clamps to about 16px -- so the rest weight is 1.28px and 1.5x of it is
       1.92px. That is a 0.64px change: sub-pixel on a 1x display and barely a
       pixel on a 2x one. Codex's own 0.97 -> 1.53 is a 0.56px delta for the same
       reason, and it only READS as a state change in their artboard because of
       the render scale it was measured at. Joy, on preview: "It's still not
       obvious at all." At 2x the delta is 1.28px, double, and lands on a whole
       pixel at every scale.

       Still calc'd off the token rather than hardcoded, so the ratio survives
       any change to the rest weight -- css-reset.css:339 keeps that handle for
       exactly this. */
    text-decoration-thickness: calc(var(--link-underline) * 2);

    /* BOLDER WITHOUT REFLOW, and this is the second version of the bold Joy
       asked for on 2026-10-28 -- "can we make the text bold as well? It's still
       not obvious at all." The first shipped `font-weight: var(--font-bold)`,
       which is a real 600 on a real variable axis (Inter loads with a 100..900
       range, document_head.hbs:293) and is therefore A METRIC CHANGE: a heavier
       weight carries wider advances, so hovering a link mid-paragraph pushed the
       text after it along and could re-wrap the line. Joy, having seen it: "try
       the switch."

       WHY text-shadow AND NOT -webkit-text-stroke, which is what was offered.
       Both avoid reflow, because both paint outside the layout box rather than
       changing advance widths. But the stroke is centred on the glyph OUTLINE,
       so half of it grows inward and eats the counters -- the enclosed holes in
       e, a, o, which at --step-1's ~16px are only a few pixels across to begin
       with and go muddy fast. A shadow paints BEHIND the fill, so the fill
       covers the inward half and only the outward half is ever visible. Same
       idea, cleaner result, so the stroke was dropped before it shipped.

       THE FACE IS INTER, not Open Sans. An earlier version of this note worked
       its arithmetic in Open Sans and Joy caught it: "that isn't being used
       anywhere, is it? Our prose text should be Inter." Correct -- style.css:97
       sets the document's family to --brand-font-family-sans ('Inter', Verdana,
       sans-serif) and style.css:50 records the move off Open Sans on 2026-10-25,
       with --ff-primary and the Open Sans entry in the Google Fonts request both
       left behind as dead weight. Callouts pin no family of their own, so this
       text is Inter by inheritance. Worth knowing before porting any figure from
       an older comment: the two faces are not interchangeable for measurement,
       Inter's zero advance is 0.6309em against Open Sans's 0.5718em, a 10.3%
       difference (css-utilities.css:751).

       THE ARITHMETIC. Two copies, offset horizontally either side, zero blur so
       the edges stay crisp rather than fuzzy. --step-1 clamps to 15.75-16.00px,
       so 0.025em is almost exactly 0.4px a side and the stems gain ~0.8px in
       total. That figure is the whole justification -- it was tuned on preview
       rather than derived from Inter's stem widths, because what mattered was
       clearing the bar a real 600 had already failed, and Joy confirmed it
       there: "Yes that's much better!" Do not re-derive it from a weight-axis
       comparison; it is deliberately heavier than a true 600.

       HORIZONTAL ONLY, which is a faithful imitation rather than a shortcut:
       type designers add most of a bold's weight to the VERTICAL stems, so
       widening sideways is what the eye reads as weight. Offsetting vertically
       as well would thicken the horizontals too and read as a blur.

       currentColor IS WHAT MAKES THIS WORK IN BOTH TIERS FOR FREE. The rule
       above pins `color` to the callout's own --fg, so the shadow is always
       exactly the ink it is thickening and no severity needs a value of its
       own. That was written when dark mode ran on strong emphasis, where the
       ink was --fixed-white or --fixed-black; since 2026-10-29 both tiers are
       the severity's own ink, which is strictly less for this to cope with.

       IT IS SAFE TO TRANSITION, unlike the font-weight it replaces. There is no
       reflow to animate, so easing it in alongside the underline reads as one
       gesture instead of a snap; it is in the transition list below. Note that
       text-shadow paints the text decoration as well, so the underline picks up
       a little extra weight from this too, which is the direction this rule
       already wants. */
    text-shadow:
      0.025em 0 0 currentColor,
      -0.025em 0 0 currentColor;
  }

  pre {
    display: flex;
    font-size: 0.75rem;
    color: var(--neutral-text);
    white-space: break-spaces;
  }
}

/* NO --fg-hover IN ANY VARIANT, and the absence is deliberate -- these five
   rules carried one each for about an hour on 2026-10-28 and it is recorded on
   the `a:hover` rule above why none of them survived. The hover inside a callout
   is not a colour. */

.callout-note,
.callout-support {
  --fg: var(--info-text);
  --bg: var(--info-bg);
  --accent: var(--info-border);
  --icon: var(--info-icon);
}

.callout-support {
  align-items: center;
}

.callout-tip {
  --fg: var(--success-text);
  --bg: var(--success-bg);
  --accent: var(--success-border);
  --icon: var(--success-icon);
  /* SETTLED 2026-10-28, KEEP THIS INK: #476827 on #e0fdcf is 5.83:1, which
     clears AA. Joy: "I say we've done what we can, so let's leave it at the
     current ink and we can adjust if need be."

     Earlier revisions of this comment called 5.83:1 a failure against "the AAA
     floor this project runs at". That framing was wrong and Joy corrected it the
     same day: "AA is our requirement, but I like to hit AAA when at all
     possible." So AAA is the target to quote and aim for, not a gate -- this ink
     meets the requirement and is not a defect to be fixed.

     It is what every word in a tip callout is painted with, not just links, so
     if it is ever revisited that is a component-wide ink change and not a link
     tweak. Unlike the dark `remember` and `warn` fills, this one is not at a
     ceiling: lime has lighter steps, so a higher ratio is reachable -- but this
     is already the lightest step of lime that clears 4.5:1 on this fill, so
     going higher means moving the fill as well.

     It is also why `tip` had no colour hover available in EITHER direction:
     this ink is already the lightest step of lime that clears even 4.5:1 on its
     own fill, so there was nothing lighter to hover to at any floor. */
}

/* `--warning-text`, on purpose and not a typo: Codex pairs `high`'s surface
   with `warning`'s ink (#613100), not with `--high-text` (#a43700). Checked on
   every `high` instance in both the Documentation and the Components frame, so
   it is the component's own definition rather than one example drifting, and
   it holds in dark too (#fcca4e, which is `--warning-text`'s dark value, on
   `--high-bg`'s #501b00). Both inks pass AA on `--high-bg` -- #613100 at
   9.8:1, #a43700 at 6.1:1 -- so following Codex here costs no contrast, and
   referencing the role rather than the hex gets the dark pairing for free. */
.callout-remember {
  --fg: var(--warning-text);
  --bg: var(--high-bg);
  --accent: var(--high-border);
  --icon: var(--high-icon);
}

.callout-warn,
.callout-warning {
  --fg: var(--critical-text);
  --bg: var(--critical-bg);
  --accent: var(--critical-border);
  --icon: var(--critical-icon);
  /* THIS IS THE VARIANT THAT EXPOSED THE WHOLE PROBLEM. --critical-text is
     --color-cherry-12 and --brand-text is --color-vermillion-12 -- the same step
     index on adjacent red hues -- so the old sitewide hover repainted almost
     exactly the ink already here, dE00 3.2 with dL* +1.0. Joy: "in some of the
     callouts you cannot see any difference at all on hover." See `a:hover`. */
}

/* The `border` this used to restate is gone: it hardcoded a different green
   from its own `--accent`, so the prop was dead and the base rule's border was
   being overridden with a value nothing named. Both now resolve to
   `--success-border`. No template renders `.callout-banner` -- and it is kept
   anyway, because article authors can be using it in content we cannot see. */
.callout-banner {
  --fg: var(--success-text);
  --bg: var(--success-bg);
  --accent: var(--success-border);
  --icon: var(--success-icon);
  margin-top: 5rem;
}

/* `.callout-banner svg` is gone with the base rule's new size. Its third
   declaration, `border-radius: 0.5rem`, was dead on arrival in any case -- the
   icon has no background to round off. */

/* DARK MODE USES CODEX'S REGULAR EMPHASIS -- THE SAME ROLES LIGHT MODE NAMES --
   WHICH IS WHY THIS SECTION HAS NO DARK BLOCK IN IT AT ALL. The four variant
   rules above reference severity ROLES rather than hexes, and every role they
   name has a dark tier, so dark mode is already right with nothing stated. The
   absence is the finished state, not an omission; css-page-error.css says the
   same thing about itself.

   THIS REVERSES THE 2026-10-25 DECISION, which was Joy's taste call -- "I much
   prefer the strong severity look for dark mode on callouts. So we'll keep the
   regular emphasis styles for light mode, but the strong emphasis on dark" --
   and which shipped as four rules pointing --fg/--bg/--accent/--icon at the
   `-bg-strong` and `-border-strong` fills with --fixed-white or --fixed-black
   ink. Joy, 2026-10-29: "I was told by my team that I needed to use the regular
   dark mode color scheme for callouts instead of the strong." Scope sitewide,
   confirmed in the same exchange.

   IT WAS ALREADY FLAGGED OPEN HERE BEFORE THE TEAM ASKED, and the flag named the
   reason: `.callout-remember` and `.callout-warn` failed the AAA target in dark
   at REST, 4.58:1 and 6.19:1, and those were the MOST their strong fills allowed
   -- #d14600 and #c30e11 cap any ink at 4.59:1 and 6.19:1, so no ink could have
   fixed them and only the emphasis choice could. Regular emphasis was measured
   then at ceilings of 13.50 / 9.16 / 14.02 / 15.70. This is that change.

   MEASURED ON THE DARK ARTICLE CARD, actual role inks rather than ceilings:

     variant        ink on fill                     was (strong)
     note/support   #73ceec on #023249   7.57 AAA    7.88 AAA
     tip/banner     #b2f27e on #364f1d   6.93 AA    10.67 AAA
     remember       #fcca4e on #501b00   9.14 AAA    4.58 AA
     warn/warning   #fc9c9e on #4f0102   7.77 AAA    6.19 AA

   EVERY VARIANT CLEARS AA, so there is no accessibility argument to be had here:
   the two that were flagged as missing the AAA target are now the two best, and
   `remember` more than doubles. The cost is real, small, and all in `tip`, which
   drops from 10.67 to 6.93 -- 0.07 short of AAA, and still ahead of the light
   tier's own tip ink at 5.83:1. `note` gives up 0.31 and stays AAA.

   THE ICONS CARRY THE SEVERITY AND ALL FOUR CLEAR THE 3:1 NON-TEXT THRESHOLD:
   5.49 note, 6.09 tip, 4.79 remember, 4.14 warn. That matters more than it looks
   -- see the next paragraph.

   THE PANELS GO QUIET AGAINST THE PAGE, AND THAT IS WHAT REGULAR EMPHASIS IS.
   These fills sit 1.04-1.59:1 from the dark article card, so the panel edge is
   carried by its border, and two of the four borders are themselves under 3:1
   against that card (note 1.85, warn 2.36; tip 4.54 and remember 3.19 pass).
   That is not a 1.4.11 failure -- the severity is conveyed by the icon and the
   text, both of which pass, and the boundary is decoration -- but it is the
   visible difference, and it is Codex's own published treatment rather than
   anything of ours: these are the identical roles light mode uses.

   --high-icon HAS NO DARK TIER, which this change exposes for the first time.
   `.callout-remember` reads it, so in dark it resolves to the LIGHT value
   #fd681e. That renders correctly -- 4.79:1 on --high-bg, comfortably past 3:1
   -- so nothing is broken, but it is the one value in this component that is not
   a dark-tier reference. It is a gap in the token file rather than something to
   patch here: that tier mirrors Codex's shipped dark stylesheet role for role,
   so if Codex ships a dark `high/icon` it belongs there. Do not invent one.

   THE ICON CHIP IS STILL REJECTED AND THE REASON OUTLIVES STRONG EMPHASIS.
   Codex sits its STRONG icon in a round 48px chip filled with the severity's
   regular surface; that was built and rejected on 2026-10-25 -- Joy: "What if we
   kept it simple and the icon would just change to the same color as the text,
   and then we wouldn't need to mess with a border at all?" The measurement
   behind it stands too: a chip is 2em where a bare glyph is 1em, which on
   new_request_page's `.callout-support` (align-items: flex-start, --step-5 icon)
   doubled the icon past its single line of text and left the text riding at the
   top of a panel the icon had stretched. Regular emphasis has no chip in Codex
   either, so the question no longer arises; the record is kept because
   `.kb-banner-icon` in css-page-home.css cites it.

   THE HOVER IS STILL NOT A COLOUR, AND THIS DOES NOT REOPEN IT. The callout
   hover is Codex's underline weight, Joy's call on 2026-10-28 off a three-way,
   and the `a:hover` rule above carries the reasoning. One strand of it WAS
   tier-specific and is now obsolete: strong emphasis pinned the rest ink at
   --fixed-white/--fixed-black, already at the ceiling, so a hover ink had
   nowhere to go, and a brute force of every step of each severity ramp found
   ZERO steps clearing 7:1 for note, remember or warn. Regular emphasis has
   headroom. It does not revive the colour hover, which was rejected on look as
   much as on numbers -- Joy: "in some of the callouts you cannot see any
   difference at all on hover." The underline needs no dark block because it
   never touches colour, which is why removing the one that stood here costs
   nothing.

   NOTHING IN THE THEME CARRIES DARK STRONG EMPHASIS ANY MORE. The one other
   place that did was `.kb-banner` on the home page -- Codex's Message at
   severity=info, but not a `callout-*` class, so it had its own
   `:root[data-theme='dark'] &` rules in css-page-home.css and this change could
   not reach it. Flagged rather than assumed, and Joy closed it immediately: "No
   you need to change the home page as well." Both dark blocks there are gone the
   same way these were, leaving role references that already resolve per tier.
   Its own comment carries its numbers, which differ from the table above only in
   the surface -- the banner sits on the page wash, not the article card. */
/*!SECTION - Callouts */

/*SECTION - Back-to-Top */

/* This box is not the button -- it is the *track* the sticky button below runs
   in. Its containing block is <main> (style.css:30 is the only
   `position: relative` in the chain), and the footer renders outside <main>, so
   whatever `bottom` says here decides whether the button can travel into the
   footer band.
   `bottom` was -5em, i.e. the track deliberately ran 5em past the end of the
   content. Combined with the z-index below, the button then rode on top of the
   footer: at narrow widths it landed on the footer wordmark, which sits in the
   same lane the button does. Wide viewports hid the collision rather than
   avoiding it, because the footer's left column starts further in.
   A positive value ends the track that much short of <main>'s bottom edge, and
   that edge is effectively the footer's top border: #page-container's
   `margin-block-end: 5rem` (style.css:44) does *not* collapse out of <main>,
   because on every template #page-container is a grid item of a .grid-content
   parent and grid items' margins never collapse. So <main> ends after that 5rem
   and the footer band begins immediately. `bottom: 0` therefore parked the
   circle flush against the border, and --space-s still read as touching it --
   Joy, 2026-10-23: "I'd like to bump the arrow up a little, currently it's
   brushing up against the top of the footer." --space-l is one step up the same
   scale. Nothing here needs to know the footer's height or layout.

   `left: 1rem` IS THE DESKTOP POSITION AND IS DELIBERATE -- do not "align it to
   the content column." That was built on 2026-10-23 and reverted the same day,
   and the reason is worth keeping because the mistake is an easy one to make
   again from a half-read request.
   Joy asked to "left align it" and, asked whether she meant the content
   column's edge rather than the window's, said yes. Built that way, it was
   wrong twice over. Her clarification: "I just meant in mobile screens. My
   issue with the arrow in section and category pages was that it was positioned
   over the footer instead of in the main body (like on the homepage)." So the
   desktop complaint was never horizontal at all -- it was the page-height bug
   the `top: 0` paragraph below covers, and the home page's 1rem lane is the
   reference she wanted kept. Moving the column inward on desktop moved the home
   page's button too, away from the thing being matched.
   The mechanism it used is also recorded, because the idea is sound in itself
   and may be wanted for something else: the column's left edge is not a number
   this rule could hold -- it is --padding-inline plus whatever the breakout
   track absorbed, and it shifts again when the docked Forethought panel widens
   --padding-inline-end (css-page-forethought.css:290). Rather than restate that
   math, the wrapper spanned <main> with `left: 0; right: 0` and carried
   `grid-content`, and the link landed in the `content` column via
   css-utilities.css:785 -- the same rule and the same width that place
   #page-container's children. It cost `grid-content` on the wrapper in all
   seven templates. If it is ever revived, note that the wrapper must then
   declare no `display` of its own, both so the class is not overridden and
   because that is what lets the single `auto` row stretch to the track's full
   height and give the sticky button somewhere to travel.
   For the mobile half of what Joy actually asked for, see the media query at
   the foot of this section.

   `top: 0`, NOT `top: 100vh` -- AND THAT IS THE FIX FOR A REAL BUG, not a
   tidy-up. Joy, 2026-10-23: "the location of the scroll to top arrow is lower
   on the category and section pages than on the homepage. I'd like to stick to
   what we have on the homepage."

   There is no per-page back-to-top rule anywhere in the theme -- this one block
   is every page's -- so the difference was never a style, it was page height.
   A sticky box is clamped to its containing block, and `top: 100vh` started
   this track one full viewport below <main>'s top, so the button could only
   reach the sticky offset below once the document was tall enough to scroll it
   there. The home page is long and so it did. The category page (hero, three
   section cards, three update cards) and a short section page are barely taller
   than the viewport, so their track began at roughly the end of the document
   and the button spent its whole life parked at the track's lower end, down by
   the footer. The shorter the page, the lower the button -- which is exactly
   the symptom, and it would have been worse on a tall monitor.

   With the track starting at <main>'s top, the sticky offset is reachable at
   any scroll position on any page, so every page now rests the button where the
   home page rests it: `calc(100vh - 5rem)`, the one number that decides the
   height, stated once below. The `bottom` clamp still does its job -- scroll to
   the end of a long page and the circle rises off the footer exactly as before
   -- and on a page too short to scroll at all the clamp is what positions it,
   just above the footer, which is the sensible answer there.

   ONE DELIBERATE BEHAVIOUR CHANGE: the button is now visible at the top of the
   page instead of appearing after about 80px of scroll. That delay was a side
   effect of `top: 100vh`, not a decision -- and it was the same mechanism as
   the bug, so it cannot be kept without reintroducing the page-height
   dependency. Making it hide at the top *and* sit at a fixed height needs a
   scroll-driven animation or a scroll listener; worth doing if Joy wants the
   delay back, but it should not be the thing holding the position fix. */
.back-to-top-wrapper {
  position: absolute;
  top: 0;
  left: 1rem;
  bottom: var(--space-l);
  pointer-events: none;
  z-index: 1000;
}

/* The circle is now drawn by the <a> itself rather than by the <svg> inside it,
   so it can carry a border and shadow the glyph box can't. Same 3.75rem round
   container as .kb-icon-graphic on the home page (css-page-home.css) -- these
   are meant to read as the same shape. Interaction states moved off the svg
   onto the anchor to match; the previous svg:focus rule was dead either way,
   since an <svg> never receives focus, the anchor does, and the sitewide
   *:focus-visible in css-reset.css already covers it.

   `top: calc(100vh - 5rem)` IS THE ONE NUMBER THAT SETS THE BUTTON'S HEIGHT on
   every page -- 5rem down to its top edge from the foot of the viewport, so the
   3.75rem circle rests with 1.25rem under it. To raise or lower the button,
   change this and nothing else. It only behaves that way on short pages because
   the track above starts at <main>'s top rather than 100vh into it; the comment
   there has the whole story.

   The doubled `position` is a fallback pair, not a mistake: `fixed` is the
   declaration a browser without sticky support keeps, and it lands the button
   in the same place -- it just loses the clamp that holds it off the footer. */
.back-to-top-link {
  position: fixed;
  position: sticky;
  pointer-events: all;
  top: calc(100vh - 5rem);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.75rem;
  height: 3.75rem;
  aspect-ratio: 1 / 1;
  border: var(--border-width-regular) solid var(--neutral-border-alpha-strong);
  border-radius: var(--radius-round);
  background: var(--neutral-bg-component);
  box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
    var(--shadow-regular);
  color: var(--neutral-icon-strong);
  transition:
    background-color 200ms ease-in-out,
    box-shadow 200ms ease-in-out,
    color 200ms ease-in-out;

  svg {
    font-size: 1.875rem;
  }
}

/* Lifted from Codex's own `.kb-button-regular:hover` (read out of the
   inspector -- it isn't documented): the hover state is a background step plus
   a taller, softer shadow. --shadow-hover is the same color as
   --shadow-regular, so the only thing that actually changes is the geometry --
   2px/6px becomes 6px/16px, which reads as the button rising off the page.

   That shadow growth replaces the old `transform: scale(1.1)`, which was
   solving the same "lift on hover" problem a different way; keeping both would
   double it. Timing is Codex's .2s ease-in-out, not the theme's 600ms-out /
   --transition-duration-in split this rule used to have. */
.back-to-top-link:hover {
  background: var(--neutral-bg-component-hover);
  box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
    var(--shadow-hover);
}

/* Codex's own active state (.kb-button-regular:active) drops the shadow
   outright rather than returning it to the resting 2px/6px, so press is the
   exact inverse of the hover lift -- the button goes from raised, to flat, to
   below flat. */
.back-to-top-link:active {
  background: var(--neutral-bg-component-active);
  box-shadow: none;
}

/* Same Codex focus treatment as the homepage cards (css-page-home.css), but it
   has to live here because this button is sitewide chrome and sits outside
   .home-page.

   The colour override this rule was created for is gone: it existed because the
   ring's sitewide default was pale salmon (--clr-primary-20) and this button is
   a pale teal circle, so the ring was technically present and effectively
   invisible. The default became --form-focus-ring sitewide on 2026-10-02, which
   fixes that at the source. Only the offset and the shadow/transition handling
   still need saying here.

   Ordered after :hover/:active deliberately: those are (0,2,0) too, so source
   order is what lets the ring survive on a button that's hovered and focused
   at once. */
.back-to-top-link:focus-visible {
  --outline-offset: 2px;
  box-shadow: none;
  transition: none;
}

/* ON MOBILE THE BUTTON LEAVES THE FLOATING LAYER ENTIRELY and becomes a normal
   centred control at the end of the page. Added 2026-10-23, reported by Joy off
   the categories-page preview: the circle was sitting on top of the Recent
   updates cards.

   The cause is arithmetic rather than anything subtle, and it is not specific to
   that page. `left: 1rem` plus `width: 3.75rem` puts the circle in the lane from
   16px to 76px. On a wide viewport that lane is inside .grid-content's left
   gutter, which is ((100vw - min(71.5rem, ...)) / 2) and hundreds of pixels wide,
   so the button floats harmlessly beside the content. On a phone the same gutter
   is just --padding-inline, i.e. --gap-sm, which clamps to 1.5rem/24px -- so the
   content column starts at 24px and the circle overlaps it by ~52px. Every page
   with content at the left edge had this: the home page's cards, the search
   results, the section page. Only the viewport width decided whether it showed.

   Note the comment above this block: `bottom: -5em` once put this same circle on
   top of the footer wordmark, which also "sits near the left gutter in the mobile
   layout -- the same 1rem lane `left` puts the button in". That was the same
   collision in a different place. Reserving a ~60px lane on a 320px screen was
   considered and rejected (Joy's call, from three costed options): it would have
   narrowed every card, heading and paragraph on every mobile page to protect one
   button.

   What the three declarations do, in order:
     - position: static takes the wrapper out of the absolute/sticky layer. It is
       a child of <main> (Zendesk's own wrapper, `position: relative` in
       style.css:53), which is a plain block rather than a grid, so in flow the
       wrapper simply becomes the last block in <main>, after #page-container.
     - `inset: auto` is defensive rather than load-bearing, and worth being
       honest about: a static box ignores top/left/bottom outright, so those
       three declarations above are already inert here. It is stated so that the
       rule reads as "this box is not positioned" rather than leaving a stale
       `top: 100vh` to be puzzled over, and so that flipping the position value
       back during some future edit cannot silently reactivate them.
     - pointer-events goes back to auto because the `none`/`all` pair above only
       exists so the full-height invisible sticky track does not eat clicks. In
       flow there is no track to eat them.
   The link itself only needs its `top` dropped; its box, colour and every state
   above are untouched.

   SPACING: THE NEGATIVE TOP MARGIN IS THE WHOLE TRICK OF THIS BLOCK, and it is
   not a nudge -- it is what makes the mobile bottom gap identical to the
   desktop one on all seven templates without a single page-specific rule.
   Do not "simplify" it to a positive value; the section below on the two pages
   that paint a background is why.

   Three reports over two days, all one piece of arithmetic.

   1. Joy, 2026-10-24: "the back to top button is still too low on the page in
      mobile, but the left position is good." There was no bottom margin here at
      all, so the circle sat on the footer's top border. The
      `bottom: var(--space-l)` added above on 2026-10-23 for that exact symptom
      ("brushing up against the top of the footer") NEVER REACHED THIS
      BREAKPOINT -- a static box ignores `bottom` outright, which the
      `inset: auto` note below already says of the positioned declarations
      generally. This is that fact biting.

   2. Then: "why is there more margin/padding on the bottom of the pages in
      mobile vs in desktop? I prefer the spacing on desktop and would like to
      keep it uniform." Measurement rather than taste, and almost exactly 2x,
      because THE BUTTON CHANGES POSITIONING SCHEME HERE. On desktop it is
      absolute, out of flow, contributing no height, so the page's bottom gap is
      only #page-container's `margin-block-end: 5rem` (style.css:138) = 80px. In
      flow it adds its own 48px plus a margin each side, and its top margin
      collapses INTO that 5rem rather than replacing it: 80 + 48 + 33 = 161px at
      390px wide.

   3. "Yes we definitely need to set the search_results_page to the same as the
      others. I don't know why it is styled differently, but these layouts
      should be default for every page." This is what rejected the first fix and
      produced the one here.

   THE FIRST FIX WAS WRONG AND IS WORTH KNOWING ABOUT. It zeroed
   #page-container's margin at this breakpoint and had this wrapper carry the
   full 5rem out of its own two margins, `calc((5rem - 3rem) / 2)` a side. Six
   templates came out right. The two that painted a background came out worse:

     - The wash on the home page and the search results page was painted on a
       box that ENCLOSED #page-container's bottom margin -- `.home-page` was an
       outer <div> wrapping #page-container (home_page.hbs:13 around :61), and
       `.search-page-wrapper` is #page-container itself with that margin restated
       as `padding-block-end` (css-page-search-results.css). Both arrangements
       kept the 5rem inside the painted box.
     - THIS WRAPPER IS A SIBLING OUTSIDE THAT BOX on both pages. So any gap it
       supplied from its own margins was unpainted, and the flat
       --page-bg-middle on <html> showed through as a pale band above the footer.
       That is the 2026-10-22 bug, and Joy reported it again here.
     - Moving the 5rem out of #page-container therefore moved 80px of gap from
       inside the painted box to outside it -- the wrong direction on exactly
       those two pages. It also shrank the home page's lower-right glow, which
       is sized as a percentage of its positioning area.

   THE WASH MOVED ON 2026-10-25 and that paragraph is now history, so read it as
   the reason this rule has the shape it has rather than as a current hazard. The
   corner glows are sitewide now, painted on <main> in css-reset.css -- which
   encloses #page-container's margin AND this wrapper, on every template. So the
   unpainted band cannot recur, and the only surviving argument against the first
   fix is the one that is enough on its own: it moved the gap instead of making
   it uniform, and what is below already lands every template on 80px.

   WHAT THIS DOES INSTEAD: #page-container keeps its 5rem everywhere, and the
   button is pulled UP into it rather than added below it. `margin-block-start`
   is the negative of the circle plus one gap, so the wrapper's net contribution
   to page height is zero and the 5rem is the whole budget. It resolves two
   different ways, both landing on 16px / 48px / 16px:

     - On five of the templates this margin ADJOINS #page-container's and they
       collapse. Collapsed margins of mixed sign are
       `max(positives) + min(negatives)`, so 5rem against -4rem is 1rem of real
       gap above the circle, then 1rem below it: 80px total.
     - On the home page and search results there is nothing for this margin to
       collapse against: on the home page #page-container is a grid item of
       `.home-page`, and grid item margins never collapse; on search results the
       5rem is padding rather than margin. The -4rem then applies in full and
       pulls the circle up, landing 16px clear of the content above and 16px
       clear of the footer -- the same rhythm by a different route.

       Both of those arrangements predate the sitewide wash and NEITHER IS ABOUT
       PAINTING ANY MORE, which is worth saying because the note above makes it
       look that way: the wrapper on `.home-page` and the margin-to-padding swap
       on search results are now just two boxes that happen not to adjoin. The
       arithmetic is unchanged either way.

   Two consequences to keep in mind when editing:
     - THE 5rem HERE IS A RESTATEMENT OF style.css:138, not a token. The two
       files must change together; that rule's comment says the same thing back.
       The custom properties below exist so the circle's size is stated once
       rather than three times, which is the part most likely to be retuned.
     - The circle has 1rem of air above it rather than the old 5rem. That is the
       only cost of matching desktop, and it is unavoidable: 5rem - 48px is 32px
       and that is the entire budget to divide.

   An earlier version of this comment claimed #page-container's margin supplied
   the gap BELOW the button. It does not -- #page-container is this wrapper's
   PRECEDING sibling, so that margin lands above the circle. That error is why
   the missing bottom margin went a round unnoticed. */

/* THE BUTTON IS LEFT-ALIGNED HERE, NOT CENTRED, and this is the whole of what
   Joy meant by "left align it" -- clarified on 2026-10-23: "I just meant in
   mobile screens." It was `justify-content: center`, which put the circle under
   the middle of the content while every other thing on these pages -- the
   breadcrumbs, the card titles, the footer wordmark -- starts at the left
   gutter.
   `flex-start` on its own would have aligned it to the window's edge rather
   than the content's, because in flow this wrapper is a plain block child of
   <main> and so spans the full width. `padding-inline-start: var(--gap-sm)`
   supplies the gutter, and that is the content column's left edge *exactly*
   rather than a value picked to look close: --gap-sm is what .grid-content sets
   --padding-inline to (css-utilities.css:703), and below 45em the content track
   is always `100% - (--padding-inline + --padding-inline-end)` with both
   gutters at their minimum, so the column starts at one --gap-sm. Same token,
   so the two cannot drift apart.
   (This is also why `grid-content` on the wrapper -- briefly added for a
   desktop version of this, see the top of the section -- was not worth keeping
   just for mobile: one padding declaration against a class on seven
   templates.)

   44.9em rather than 45em: the theme's established mobile edge, paired with the
   `min-width: 45em` queries used in thirteen places. */
/* THE WHOLE OF THIS MEDIA QUERY WAS DEAD FOR TWO COMMITS, AND THE CAUSE WAS AN
   EARLY COMMENT TERMINATOR ABOVE, NOT ANYTHING IN THESE RULES. Keep this note:
   the symptom impersonated a cascade bug well enough to cost a round.
   (And no, the sequence cannot be written out here -- it would close this
   comment at that point and break the block all over again.)

   Joy, 2026-10-24: "the responsive back to top button is no longer working --
   the size stays the same now." c97dd20 closed the SPACING comment early, which
   left its last few paragraphs as bare prose at the top level of the file. CSS
   error recovery on a bad qualified rule discards the prelude AND the next
   {...} block, and the next block was this entire @media. So nothing in here
   applied at all, below 45em or anywhere.

   What made it read as a size-only failure: with the mobile block gone, the
   desktop `position: absolute` + `position: sticky; top: calc(100vh - 5rem)`
   pair still parks the circle near the bottom-left of a narrow viewport. The
   spacing LOOKED right, so the only visible difference was the 3.75rem circle,
   and the obvious suspect was the var() plumbing that had just changed.

   WHAT ACTUALLY SETTLED IT -- four computed values from one console read at a
   606px viewport, which no cascade explanation survives:
     wrapper `position` absolute, wrapper margins `0px 0px`, link `width` 60px,
     and `--btt-size` empty on :root.
   A losing specificity battle changes one declaration. A whole block of them
   missing at a viewport that matches the query means the block is not there.
   When several unrelated declarations fail together, check that the file PARSES
   before reasoning about the cascade -- comment and brace balance per file is
   a two-line script and would have caught this on the commit that caused it.

   The :root placement below is NOT the fix, then, but it is kept on its own
   merits: both rules read the same declaration directly rather than the link
   depending on an inheritance hop from its parent, and that is one fewer thing
   to be wrong about next time. Same for the literal fallbacks -- see the note
   there. Still one declaration of the size, which was the point of using a
   custom property at all, and still scoped to this breakpoint: above 45em the
   button is out of flow and the 5rem is not its budget. */
@media (max-width: 44.9em) {
  :root {
    --btt-size: 3rem;
    --btt-gap: calc((5rem - var(--btt-size)) / 2);
  }

  .back-to-top-wrapper {
    position: static;
    inset: auto;
    display: flex;
    justify-content: flex-start;
    padding-inline-start: var(--gap-sm);
    pointer-events: auto;

    /* Net height contribution: -(3rem + 1rem) + 3rem + 1rem = 0. So the 5rem
       above is the entire bottom gap and mobile matches desktop. See the
       SPACING block for the two ways this resolves. */
    margin-block-start: calc(-1 * (var(--btt-size) + var(--btt-gap)));
    margin-block-end: var(--btt-gap);
  }

  /* THE CIRCLE IS SMALLER HERE, 3rem/48px against the 3.75rem/60px above.
     Joy, 2026-10-24: "is it at all possible to decrease the size of the back to
     top button responsively?" At 3.75rem the circle is about 15% of a 390px
     viewport, which is heavy for chrome that sits at the page foot.

     48px IS CHOSEN AGAINST THE 44px TAP-TARGET FLOOR, not picked by eye -- it
     leaves 4px of slack, so a future border or padding change eats spare room
     rather than an accessibility minimum. 2.75rem would have been exactly the
     floor with none. That floor is the same number the categories-page round
     cited when it rejected shrinking this button; worth noting that rejection
     does NOT apply here. Back then the button was still floating in the left
     gutter and a smaller circle was a workaround for the content overlap. In
     flow it overlaps nothing, so size is now purely a weight decision.

     The glyph steps 1.875rem -> 1.5rem to hold the same ~62% glyph-to-circle
     ratio; shrinking the box alone would have left an oversized arrow in it.
     Targeting `svg` rather than `i` is required, not stylistic -- the Font
     Awesome kit replaces the <i> in the markup with an <svg> at runtime, so a
     bare `i` selector matches nothing after load. Same (0,1,1) as the nested
     rule above, so the later source position is what wins.

     ONE STATED INTENT THIS BREAKS, flagged to Joy before the change: the
     comment on .back-to-top-link calls the 3.75rem circle "the same 3.75rem
     round container as .kb-icon-graphic on the home page -- these are meant to
     read as the same shape." Below 45em they no longer match. Accepted because
     the two never appear adjacent -- this is page-foot chrome, the chip is
     inside a card -- but if .kb-icon-graphic is ever given a mobile size, this
     is the other half of that pair.

     Nothing on desktop can be affected by these three declarations: they are
     inside the breakpoint, and the link's `top: calc(100vh - 5rem)` -- the one
     number that sets its height up there -- is already overridden to `auto`
     just below. */
  /* THE `3rem` FALLBACKS ARE DELIBERATE, not belt-and-braces noise. `width` is
     not an inherited property, so if --btt-size ever fails to resolve here the
     declaration is invalid at computed-value time and `width` becomes `auto` --
     it does NOT fall back to the 3.75rem declared in the base rule above. On a
     flex item with `aspect-ratio: 1 / 1` that means a circle sized to its
     1.5rem glyph. The fallback makes the whole failure mode unreachable and
     costs one token. (The --btt-size on :root is still the single source of
     truth; this only matters if that declaration is not reaching this element.
     That was the suspected cause of Joy's 2026-10-24 report and it was wrong --
     see the note above the breakpoint for what it actually was.) */
  .back-to-top-link {
    position: static;
    top: auto;
    width: var(--btt-size, 3rem);
    height: var(--btt-size, 3rem);

    svg {
      font-size: 1.5rem;
    }
  }
}
/*!SECTION - Back to Top */

/*SECTION - Cards */

.blocks {
  width: 100%;
}

.card-flex {
  --gap: 1rem;
  display: grid;
  grid-gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(49%, 1fr));

  & .card-article {
    flex: 1 1 48.5%;
    flex-direction: row;
    gap: 1.5rem;
    align-items: flex-start;
    padding: var(--space-s);
    height: 6rem;

    & .card-title {
      display: flex;
      gap: 1.5rem;
      width: 100%;
      justify-content: space-between;
      text-wrap: pretty;

      &:focus-within {
        border: none;
        outline: none;
      }
    }
  }
}

.card-grid--title,
.recent-activity-header {
  margin-block-end: 2rem;
}

.card-grid,
.recent-activity-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19.5rem, 1fr));
  gap: 1.5rem;
}

.card {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: var(--space-s);
  border: var(--card-border);
  border-radius: 0.5rem;
  justify-content: space-between;
  position: relative;
  cursor: pointer;

  .category {
    align-items: center;
  }

  &:hover {
    border-color: var(--clr-neutral-40);
    box-shadow: var(--shadow-dropdown);
    cursor: pointer;
    transition-duration: var(--transition-duration);
  }

  .title {
    gap: 1rem;
    display: grid;
    grid-template-rows: 5rem 3rem 1fr;
    justify-items: center;
    color: var(--clr-neutral-160);
    .card-icon {
      height: 5rem;
      width: 5rem;
      display: flex;
    }

    h3 {
      line-height: 1.2;
    }

    p {
      display: flex;
    }
  }

  .title-cta {
    grid-template-rows: none;
  }

  &:focus-within {
    --outline-size: max(1px, 0.15em);
    --outline-color: var(--clr-primary-10);
    --outline-style: solid;

    outline: var(--outline-size) var(--outline-style) var(--outline-color);
    outline-offset: var(--outline-size);
    border: 1px solid var(--clr-primary-100);
  }

  a {
    display: flex;
    align-items: center;
    text-decoration: none;
    gap: 0.5rem;
    color: var(--clr-neutral-160);

    &:hover {
      text-decoration: underline;
      font-weight: var(--font-bold);
      background-color: transparent;
      transition-duration: var(--transition-duration);
    }

    &::after {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 10;
    }
  }
}

.link-card::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
}

.activity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
  a {
    color: var(--clr-neutral-160);
  }
}

.recent-activity-item {
  align-items: flex-start;
  .category-item {
    color: var(--clr-success-140);
    padding: 0.125rem 0.75rem;
    margin-top: 0;
  }
}
.recent-activity-item-time {
  color: var(--clr-neutral-100);
  font-size: var(--step-0);
  z-index: 100;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: var(--font-bold);
  &::after {
    display: none;
    font: var(--fa-font-solid);
    content: '\f054';
  }
}

.recent-activity-item:hover * {
  color: var(--clr-neutral-160);
  transition-duration: var(--transition-duration);
}

.activity a:hover {
  background-color: transparent;
  transition-duration: var(--transition-duration);
}

.help-title {
  color: var(--clr-neutral-160);

  &:hover {
    text-decoration: underline;
    transition-duration: var(--transition-duration);
  }
}

[dir='rtl'] a.help-title > h3 > svg,
[dir='rtl'] a.link.link-footer > svg,
[dir='rtl'] .btn-theme-footer > svg {
  transform: scaleX(-1);
}

.help {
  background-color: var(--clr-primary-100);
  height: 2rem;
  border-radius: 50px;
  padding: 1.25rem;
}

[dir='rtl'] .card.category > a > svg,
[dir='rtl'] .card-title > svg,
[dir='rtl'] .recent-activity-item-time > svg {
  transform: rotate(180deg);
}

/*!SECTION - Cards */

/* SECTION - Lightboxes/Fancybox */
a[data-fancybox] {
  display: block;
  cursor: pointer;
  line-height: 0;

  &:hover {
    background-color: transparent;
  }
}

img.fancybox-image {
  max-width: 85vw;
  max-height: 85vh;
}
/*!SECTION - Lightboxes/Fancybox */

/* Cluster - Useful for wrapping navigation and footer menus */
[class*='cluster-'] {
  justify-content: var(--justify, space-between);
  gap: var(--gap, 2rem);
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  align-items: center;
}

.cluster-footer-links {
  --gap: 1rem;
}

/* SECTION - Brand refresh: shared page components
   ========================================================================
   The hero band, the section heading, the Icon Graphic card and the update
   card, shared by the category page and the section page. Between them they
   are now the whole of both pages -- css-page-category.css and
   css-page-section.css are both empty stubs that point here, and their headers
   say so.

   WHY THESE LIVE HERE AND NOT IN A PAGE FILE. They were written for the
   category page on 2026-10-23 and lived in css-page-category.css for exactly
   one day; the section page then needed the same components, and
   css-page-category.css's own note said that a second consumer is the point at
   which they move rather than get copied again. Two reasons this file in
   particular:

     1. Load order. document_head.hbs loads css-components.css before every
        css-page-*.css, so a page file can override anything here on equal
        specificity. The alternative -- leaving them in css-page-category.css
        and widening its scope selector -- would have put the shared base
        *after* css-page-section.css (line 221 against line 230) and left
        section-page overrides losing ties to the category page's file, which is
        a trap rather than a preference.
     2. The scope selector below keeps these names off every other page, which
        matters because four of them are deliberately shared with
        css-page-home.css -- .kb-icon-graphic, .kb-badge, .kb-updates-grid and
        .kb-update-card*. Same components, same names, on purpose.

   STILL SCOPED, THOUGH, to the four page classes below, which each template
   puts on <html> as its first statement. The scope is not about
   specificity; it is because css-page-home.css owns `.home-page`-scoped copies
   of several of these names and its treatments differ in places -- a coral
   .kb-icon-graphic against this grey one, a four-way colour cycle on .kb-badge
   against this single neutral -- and css-page-error.css owns a smaller grey
   chip again. An unscoped rule here would reach all of those and quietly add
   whatever declarations their copies happen not to state. When a further page
   wants one of these components, widen the selector list rather than copying
   the block; when the home page and error page chips are reconciled with this
   one (they differ in fill and in diameter, not in structure), the scope can
   go altogether.

   `html.requests-page` IS THAT THIRD CONSUMER, added 2026-10-24 when Joy asked
   for the "My Requests" hero to take the category and section treatment. It
   takes the widening route the paragraph above prescribes, and it is safe to
   widen rather than split because the requests template uses none of the other
   names in this block -- checked, the intersection of its class list and the
   eighteen selectors here is empty, so the only rules it picks up are the four
   .kb-page-hero* ones it is asking for. Two consequences if that stops being
   true: a requests-page `.kb-section-grid` or `.kb-badge` would silently
   inherit the category page's treatment, and that is the point at which this
   block should be split by component rather than widened again.

   `html.new-request-page` IS THE FOURTH, added 2026-10-25 for the same reason
   one level along: that page's hero was still the pre-refresh `bg-neutral-5`
   band, which has one value in both themes and left its title sitting on a
   near-white strip in dark mode. Same check run before widening -- the
   intersection of new_request_page.hbs's class list and the eighteen selectors
   here is empty apart from the four .kb-page-hero* ones, so the only rules it
   picks up are the ones it is asking for. Its own stylesheet states no `.kb-*`
   name at all, so there is nothing for the load order to decide either.

   `html.article-page` IS THE FIFTH AND LAST, added 2026-10-26. That page held
   the final pre-refresh `bg-neutral-5` band on the site -- same one-value-in-
   both-themes defect as new_request's, plus `.hero`/`.hero-content` matching no
   rule at all there, so the band had no padding and was simply the height of its
   text. Same check run again before widening, and the intersection is empty:
   article_page.hbs's only `kb-*` class is `kb-breadcrumbs`, which is not one of
   the eighteen selectors here. ONE DIFFERENCE WORTH KNOWING -- on the other four
   pages .kb-page-hero-title dresses the page's <h1>, and on the article page it
   dresses a <p>, because the <h1> there is the article title further down. These
   are all class selectors, so that costs nothing; just do not add an `h1` or
   `:where(h1)` qualifier to the title rule or the article page silently loses it.

   All five selectors are (0,1,1), so every nested rule below is (0,2,1) --
   exactly what these rules carried in css-page-category.css, so nothing about
   the cascade changed when they moved, or as pages joined.

   DARK MODE NEEDS NO RULES HERE. Every colour is a Codex semantic role from
   css-brand-tokens.css and each of those carries a light and a dark value, so
   [data-theme='dark'] flips them with nothing stated. Reaching for a --clr-* or
   --color-* primitive instead of a role breaks that silently, because a
   primitive is one value in both themes by definition.
   ===================================================================== */

html.category-page,
html.section-page,
html.requests-page,
html.new-request-page,
html.article-page {
  /* ---- Hero ----
     The band is `grid-content full-width` in both templates, which makes it
     both a full-bleed row of the page grid *and* a copy of that grid itself.
     That second part is doing real work: css-utilities.css:785's
     `.full-width > :not(.breakout, .full-width)` places the band's own child
     into the centred `content` column, so the background runs edge to edge
     while the text lines up with the breadcrumbs below it. For free, with no
     width or margin stated here.

     Which is why this rule must not set `display: flex`. The home page's hero
     does (css-page-home.css:113) and then has to put back what that cost --
     `.hero-inner { width: min(34.3rem, 100vw - 2rem) }` exists because flex
     dropped the grid placement. Here the flex column is the *inner* element
     instead, and the band keeps the `display: grid` it inherits from
     .grid-content.

     --neutral-bg-soft, not the --clr-neutral-5 both bands used to carry: a role
     with both tiers (#ebf8f8 light, #172029 dark) rather than a primitive with
     only one. It reads as a tint of the page in light mode and as a lift off it
     in dark, which is the right relationship in both. */
  .kb-page-hero {
    padding-block: var(--space-2xl-3xl);
    background-color: var(--neutral-bg-soft);
  }

  /* Type only. The category hero carried an icon chip beside this column for
     one round and Joy removed it on 2026-10-23 after preview, so the section
     page never got one. The wrapper itself still has to exist -- it is the
     child that css-utilities.css:785 places into the centred `content` column,
     which is what aligns the title with the breadcrumbs under a full-bleed
     band.

     IT HAS HELD ONE CHILD SINCE 2026-10-30, when the hero description was
     removed from both templates, so the flex column and its gap currently
     space nothing. Both are kept: the column is what the removed description
     and any future second line would sit in, and neither declaration renders
     anything on its own. */
  .kb-page-hero-inner {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
  }

  /* --step-7, where both old heroes were --step-9 and the home page's still is.
     Deliberate: --step-9 is the site's single largest heading and belongs to
     the one hero that greets you, and a category or section name is a good deal
     longer than "Hi, how can we help?" -- "Security Awareness Training" at
     --step-9 wraps to three lines on a laptop. --step-7 is still unmistakably
     the page title. Joy can take this to her team if they want it bigger; it is
     one token, and it is now one token for both pages.

     Part of that reasoning was "and leaves room for the description
     underneath", which stopped applying on 2026-10-30 when the description came
     out and left the <h1> alone in the band. The size argument above stands on
     its own -- a long category name is why this is not --step-9 -- but if she
     wants the band to carry more weight now that it holds one line, this token
     is the lever. */
  .kb-page-hero-title {
    font-family: var(--ff-logo);
    font-weight: var(--font-bold);
    font-size: var(--step-7);
    line-height: var(--leading-heading-7);
    color: var(--neutral-text);
    text-wrap: balance;
  }

  /* .kb-page-hero-desc IS GONE, 2026-10-30. Joy: "remove the category
     descriptions from the hero section of each template -- we want just the
     category title." Both heroes dropped their `{{#if ...description}}`
     paragraph in the same change, and those were this rule's only two callers,
     so it is deleted rather than left behind as dead CSS.

     The recipe, in case a description is ever wanted back: --step-1 /
     --leading-body-1 in --neutral-text-soft, on a max-inline-size of 34.3rem.
     That measure was `60ch` until 2026-10-25 and is the one piece of this worth
     not relearning -- the Inter swap widened every ch length 10.3%, and this
     element set its own font-size, so its ch resolved against --step-1 (fluid
     15.75-16.00px) rather than the 16px the other pinned measures used. 34.3rem
     is what 60ch came to under Open Sans: a flat 548.8px where the old value
     was really a fluid 540-549px. The ch hazard itself is written up on
     css-utilities.css's measure note. */

  /* ---- Section headings ----
     Same declarations as css-page-home.css:195 so all three pages' section
     headings match. Carried rather than shared with that file because its copy
     is locked inside the `.home-page` scope. */
  .kb-section-title {
    font-family: var(--ff-logo);
    font-weight: var(--font-bold);
    color: var(--neutral-text);
    font-size: var(--step-4);
    line-height: var(--leading-heading-4);
    margin-block-end: var(--space-m);
  }

  /* Trims the half-leading above and below each brand heading so the
     surrounding gap/margin is the only thing setting its spacing. Progressive
     enhancement, and block containers only -- which is why .kb-badge
     (inline-flex) is absent here, as on the home page. .kb-update-card-title
     qualifies even where the markup makes it a <span>, because a flex item is
     blockified whatever its element's default display is. */
  @supports (text-box: trim-both cap alphabetic) {
    .kb-page-hero-title,
    .kb-section-title,
    .kb-section-card-title,
    .kb-update-card-title {
      text-box: trim-both cap alphabetic;
    }
  }

  /* ---- The Icon Graphic card ----
     Codex's "Icon Graphic" card (Figma node 21851:1926), which Joy asked for by
     name: the same component the home page's "Need more help?" cards are built
     on, but with the grey circular icon chip rather than the coral one. It
     carries a category's sections on the category page and a section's
     subsections on the section page -- the same job one level apart.

     Note the two senses of "section" that meet in these names:
     .kb-section-title is the heading of a *page* section (the home page's
     meaning), while .kb-section-card is a card for a *Zendesk* section. The
     -title / -card suffixes keep them apart; .kb-section-card-title is the
     heading inside one of the cards. */
  .kb-section-grid {
    /* Three to a row at most, matching .kb-updates-grid. Joy, 2026-10-30:
       "update .kb-section-grid so that it matches the others -- max of three
       across a row." This was `auto-fit, minmax(17rem, 1fr)` until then -- the
       last card grid on these pages that let a wide monitor put four or five
       cards in a row, which is what the 2026-10-23 note below used to describe
       as deliberate.

       IT KEEPS auto-fit, WHICH IS THE ONE WAY IT DOES NOT MATCH
       .kb-updates-grid -- Joy's call on 2026-10-30, after previewing auto-fill
       here: "let's go back to auto-fit." The cap is what she asked for; the
       thirds that come with auto-fill were not. auto-fit builds the same three
       tracks from the same minimum and then collapses the empty ones, so a
       wide row still holds three at most, but one or two cards stretch to fill
       the row instead of sitting in thirds with dead space to their right --
       and a category or section with one or two children is common.

       SETTLED, not provisional: she took it to her team the same day and they
       confirmed auto-fit. Do not swap it back on consistency grounds alone.

       The reasoning behind the two values that ARE shared -- why the minimum is
       a third of the row rather than a flat 17rem, and why the `- 1px` is not a
       fudge (it keeps the track count off a knife edge; without it the
       arithmetic can floor to 2) -- is written once, on .kb-updates-grid
       further down this file. Read it there before touching either.

       The gap is --space-s here as it is there, so the calc is a verbatim copy.
       The gap appears twice in this rule and both have to agree: the calc
       subtracts the two gaps from the row before dividing by three, so a gap
       token in the calc that the `gap` declaration does not use makes the
       column-count arithmetic wrong at the margins. */
    --kb-section-col-min: max(
      17rem,
      calc((100% - 2 * var(--space-s)) / 3 - 1px)
    );
    display: grid;
    grid-template-columns: repeat(
      auto-fit,
      minmax(var(--kb-section-col-min), 1fr)
    );
    gap: var(--space-s);
  }

  /* Declaration for declaration the same as .kb-help-card
     (css-page-home.css:586).

     THE COLUMN AND THE GAP NOW HAVE NOTHING TO DISTRIBUTE, and that is
     deliberate rather than leftover. Joy removed the card's description on
     2026-10-23 after preview -- "you need to hide section descriptions on
     cards" -- because Zendesk descriptions are optional per section, so one
     card in a row carrying a paragraph stretched the whole row and left the
     other two with dead space under their titles. The card is a single row
     again, so flex-direction and gap are inert until it has a second child.
     They stay because the second child is a `{{#if description}}` away in
     either template and this is still Codex's Icon Graphic card, which has two
     rows; stripping them would make restoring one line of markup a CSS job
     too. */
  .kb-section-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: var(--space-s);
    color: var(--neutral-text);
    border: var(--border-width-regular) solid var(--neutral-border-soft);
    border-radius: var(--radius-md);
    background-color: var(--neutral-bg);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
    text-decoration: none;
    transition: all 200ms ease-in-out;
    will-change: transform;
    backface-visibility: hidden;
    cursor: pointer;
    align-items: start;
  }

  .kb-section-card:hover {
    border-color: var(--neutral-border);
    box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
      var(--shadow-hover);
  }

  /* Shadow to none rather than back to the resting 2px/6px -- Codex's active
     spec, which makes the press the inverse of the hover lift instead of an
     undo of it. */
  .kb-section-card:active {
    border-color: var(--neutral-border-strong);
    box-shadow: none;
  }

  /* MUST STAY LAST of the three state rules. All of :hover, :active and
     :focus-visible here are (0,2,1), so source order is the only thing making
     the focus ring win on a card that is both hovered and focused.
     Colour and width come from css-reset.css's sitewide *:focus-visible, whose
     default became --form-focus-ring in October; only the offset needs pinning,
     because the reset falls back to max(2px, 0.15em) and Codex uses a flat 2px
     here. Focus drops the shadow rather than lifting it on purpose: the ring
     already says "you are here", and adding the hover lift on top muddies the
     two states. See css-page-home.css:282 for the long version. */
  .kb-section-card:focus-visible {
    --outline-offset: 2px;
    box-shadow: none;
    transition: none;
  }

  /* Fixed rem rather than the fluid scales: a decorative chip whose size is
     the component's, and the circle has to stay a circle. aspect-ratio is
     belt-and-braces for if the explicit height ever comes off.

     THE FILL IS THE ONE DELIBERATE DEPARTURE FROM THE HOME PAGE'S COPY of this
     component, and it is the thing Joy asked for: "I think instituting they
     grey circular background on the icon would be good to use."
     css-page-home.css:645 fills with --brand-bg (coral) and --fixed-white;
     this is Codex's own neutral treatment instead --
     --neutral-bg-component behind, --neutral-border-alpha around,
     --neutral-icon in front. All three are roles with both tiers, so the chip
     flips on its own: #deebec/#10171e1a/#384649 in light,
     #2e3e4e/#ffffff1a/#b2c3d2 in dark.
     --neutral-border-alpha rather than the home page's -alpha-strong: against
     a grey fill rather than a saturated coral one, the stronger value draws a
     visible ring around the chip instead of just defining its edge.

     THERE ARE THREE DIAMETERS OF THIS CHIP IN THE THEME and they are not
     reconciled: 3.75rem here, 3.75rem on the home page's help cards, 3rem on
     the error page (css-page-error.css:232, with a 1.5rem glyph). The error
     page's is scoped to `.error-page` and the home page's to `.home-page`, so
     the three never meet; unifying them means giving the chip a size custom
     property and belongs to its own round, not to this one. */
  .kb-icon-graphic {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Chip and arrow both opt out of shrinking -- a narrow card would
       otherwise squash the circle into an oval and clip the arrow's 1em
       window before the title ever wrapped. */
    flex-shrink: 0;
    width: 3.75rem;
    height: 3.75rem;
    aspect-ratio: 1 / 1;
    border: var(--border-width-regular) solid var(--neutral-border-alpha);
    border-radius: var(--radius-round);
    background-color: var(--neutral-bg-component);
    color: var(--neutral-icon);
  }

  /* svg, not i -- the Font Awesome kit is JS-only and replaces each <i> with an
     <svg>, so an `i` selector matches nothing once the page has rendered.

     THE COLOUR WAS RESTATED HERE, on the svg, and not only on the chip above,
     because `.fa-books` -- the glyph both pages' cards use -- was painted
     directly by its own rule in css-icons.css with --clr-neutral-160, and a
     declaration on the element always beats a value inherited from its parent,
     whatever the specificities are. So the chip's own `color` reached every
     icon except the one actually in the markup, and that one got a legacy
     primitive with no dark tier: a dark-grey glyph on a dark chip. This rule,
     scoped, is (0,2,2) against that rule's (0,1,0), so it won outright rather
     than on load order. It was a live dark-mode bug on the section page until
     2026-10-23, and restating the colour here is what fixed it.

     THE RULE IT WAS DEFENDING AGAINST IS NOW GONE -- `.fa-books` was deleted
     from css-icons.css on 2026-10-30 in Phase 5's legacy sweep, precisely
     because this rule meant its ink never painted. So `color` here is now a
     restatement of what the chip already passes down rather than an override of
     anything, and it is kept deliberately: it costs nothing, it keeps the glyph
     colour stated beside the `font-size` that has to live here anyway, and it
     is what makes a future `.fa-*` rule arriving with its own `color` a
     non-event. `font-size` is the declaration that genuinely cannot move. */
  .kb-icon-graphic svg {
    font-size: 1.875rem;
    color: var(--neutral-icon);
  }

  /* Chip, title and arrow share the card's top row -- the Icon Graphic card's
     structure, and the layout the home page's help cards settled on. The row
     is the card's only child since the description came off (see
     .kb-section-card above); the wrapper stays because it is what makes the
     chip, title and arrow one horizontal group rather than three children of
     a column.
     The explicit width is required: .kb-section-card sets align-items: start,
     so without it this row shrinks to its own content and the arrow has
     nothing to be pushed away from. The title carries `flex: 1` rather than
     this row carrying justify-content: space-between, which would distribute
     the free space *between* all three and leave the title floating mid-row
     instead of sitting against the chip. */
  .kb-section-card-top {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    width: 100%;
  }

  /* `flex: 1` is what holds the arrow at the far end of the row: the title
     takes all the free space, so there is none left to distribute. It also
     means a long section name wraps inside its own box rather than pushing the
     arrow out of the card. Still a block container even though it is now a
     flex item -- being an item does not change its inner display, and block is
     what the text-box trim above needs. */
  .kb-section-card-title {
    flex: 1;
    font-family: var(--ff-logo);
    font-weight: var(--font-bold);
    font-size: var(--step-2);
    line-height: var(--leading-heading-2);
    color: var(--neutral-text);
  }

  /* Two stacked copies of the chevron in a 1em box clipped by overflow: hidden.
     On hover or keyboard focus the resting copy exits and the second arrives
     from the opposite side, so the arrow reads as being replaced rather than
     drifting away. Lifted from .kb-help-card-arrow (css-page-home.css:693),
     with one change: TRAVEL IS HORIZONTAL HERE, NOT DIAGONAL.
     That is not a style preference. The home page's cards point off-site, and
     their up-and-right arrow travelling up and right is part of saying so;
     these cards point at another page of this help centre. So the glyph is a
     chevron and it moves along one axis -- the same distinction the chevron
     already made in both blocks before the refresh.

     Travel is 24px rather than the help card's 30px because the chevron sits
     at --step-4 in a 1em window; 30px overshot the clip and left a frame with
     no arrow in it at all mid-transition. */
  .kb-section-card-arrow {
    --kb-arrow-travel: 24px;
    display: grid;
    place-items: center;
    overflow: hidden;
    flex-shrink: 0;
    width: 1em;
    height: 1em;
    font-size: var(--step-4);
    color: var(--neutral-icon-soft);
    transition: color 200ms linear;
  }

  .kb-section-card-arrow svg {
    grid-area: 1 / 1;
  }

  .kb-section-card-arrow .kb-arrow-in {
    translate: calc(var(--kb-arrow-travel) * -1) 0;
  }

  /* Paired with :focus-visible, not :hover alone. On this card the arrow *is*
     the affordance -- there is no "View articles" label under it any more -- so
     a keyboard user should not be the only one who never sees it respond.
     :is() takes the specificity of its highest argument and both arguments are
     (0,1,0), so this stays (0,3,1) and the state rules' source-order
     relationships above are untouched. */
  .kb-section-card:is(:hover, :focus-visible) .kb-section-card-arrow {
    color: var(--neutral-icon-strong);
  }

  /* Motion is opt-in, matching css-reset.css / css-search.css / the home page.
     With it off the incoming copy simply stays parked outside the clip and only
     the colour changes, so no extra rules are needed to hide it. */
  @media (prefers-reduced-motion: no-preference) {
    .kb-section-card-arrow svg {
      transition: translate 200ms linear;
    }

    .kb-section-card:is(:hover, :focus-visible) .kb-arrow-out {
      translate: var(--kb-arrow-travel) 0;
    }

    .kb-section-card:is(:hover, :focus-visible) .kb-arrow-in {
      translate: 0 0;
    }
  }

  /* ---- The update card ----
     The home page's Recent updates card: badge, title, "View article >". It
     carries this category's three most recent articles on the category page,
     and this section's full article list on the section page.

     THE SECTION PAGE'S ARTICLE LIST USED TO BE A DIFFERENT COMPONENT and this
     is the round that merged them. It was one bordered box with hairline rows
     (.kb-article-list / .kb-article-row*, css-page-section.css), on the
     argument that a section holding 41 articles would be 41 shadowed cards and
     that is a scrollbar rather than a list. Joy saw it in preview on 2026-10-23
     and asked for the cards instead -- "let's make them cards in the same
     format as the recent updates cards" -- which is also the answer to the
     scale worry: the cards sit in a grid several to a row, so 41 articles is a
     dozen-odd rows rather than 41 full-width ones, and it is a shorter page
     than the row list was. (This read "repeat(auto-fit, minmax(17rem, 1fr)), so
     about eleven rows of four on a laptop" until 2026-10-30. That stopped being
     true on 2026-10-29, when `12493c0` capped the row at three -- the scale
     argument survives the change, the arithmetic in it did not, so the numbers
     are gone rather than restated and .kb-updates-grid below is the one place
     the track math lives.)

     That merge is why these rules are here rather than in
     css-page-category.css, where they were written. Same route, same reason as
     the hero and the Icon Graphic card above: a second consumer means the rules
     move, and they have to move *up* the load order rather than have
     css-page-category.css widen its scope, because that file loads at
     document_head.hbs:230 and css-page-section.css at :221 -- the base would
     have ended up after the page file that needs to override it.

     The gap is --space-s, the same as the shared section grid above and the
     same as .kb-topic-grid on the home page. It was --space-m until
     2026-10-29, deliberately pairing the two grids the way the home page did;
     Joy ended that pairing: "on the recent updates cards on every page we are
     using the wrong gap on the grid -- it should be the same as for the topic
     cards." Changed in both places that style this grid -- here and the
     .home-page copy in css-page-home.css, since the five html.*-page
     selectors this block is nested in do not include the home page.

     THREE TO A ROW, AND A SHORT ROW STILL SITS IN THIRDS. Joy, 2026-10-23:
     "when listing articles, can we have them be three to a row. When there are
     only two articles the card would still only be a third (grid setup)". The
     plain `repeat(auto-fit, minmax(17rem, 1fr))` this replaces did neither:
     auto-*fit* collapses the empty tracks and then `1fr` shares the whole row
     out between whatever cards there are, so a section with two articles got
     two half-width cards (which is what Joy was looking at), and a wide monitor
     got four or five to a row rather than three.

     Two changes fix it, and both are needed. auto-fill instead of auto-fit
     keeps the empty tracks, so two cards occupy two of three columns and the
     third stays empty rather than being shared out. And the minimum track is a
     third of the row rather than a flat 17rem, so three is the number that
     fits:

       max(17rem, (100% - 2 gaps) / 3 - 1px)

     The percentage resolves against this grid's own width, not the viewport's,
     which is the width budget this project prefers to a guessed breakpoint --
     it stays correct if the content column ever changes, and it is why there is
     no media query here. Above about 860px of grid width the third is the
     larger value and the row holds exactly three; below that 17rem wins and the
     row drops to two and then to one on its own, exactly as before. (That
     threshold is 816px + two gaps -- 3 x 17rem plus the gutters -- so it moved
     down from about 880px when the gap went from --space-m to --space-s.) The tracks
     are still `1fr`, so the minimum only decides *how many* there are -- the
     cards themselves are always equal.

     THE `- 1px` IS NOT A FUDGE, it is what keeps the count off a knife edge. A
     min of exactly (100% - 2g)/3 makes the track-count arithmetic come out at
     precisely 3.0, and a floating-point result a hair under that floors to 2 --
     two two-thirds-width cards, intermittently, depending on the viewport. One
     pixel of slack puts the result just above 3 instead. It cannot overshoot to
     4: that would need the min to fall to a quarter of the row.

     .kb-section-grid above was left out of this entirely when it landed -- Joy
     had asked for the three-up count on the article list only. She extended it
     to the subsection cards on 2026-10-30 ("max of three across a row"), so
     that rule now carries a verbatim copy of the minimum, including the
     `- 1px`. It stays on auto-fit rather than auto-fill, which is deliberate
     and is argued on that rule: she previewed both and kept the stretching
     short row there. Both grids are on --space-s, so there is only one gap
     token between them. */
  .kb-updates-grid {
    /* --space-s, matching .kb-topic-grid on the home page. The gap appears
       twice in this rule and both have to agree: the calc below subtracts the
       two gaps from the row before dividing by three, so a gap token here that
       the `gap` declaration does not use makes the column-count arithmetic
       wrong at the margins. */
    --kb-updates-col-min: max(
      17rem,
      calc((100% - 2 * var(--space-s)) / 3 - 1px)
    );
    display: grid;
    grid-template-columns: repeat(
      auto-fill,
      minmax(var(--kb-updates-col-min), 1fr)
    );
    gap: var(--space-s);
  }

  .kb-update-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    padding: var(--space-s);
    color: var(--neutral-text);
    border: var(--border-width-regular) solid var(--neutral-border-soft);
    border-radius: var(--radius-md);
    background-color: var(--neutral-bg);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
    text-decoration: none;
    text-align: start;
    align-items: flex-start;
    transition: all 200ms ease-in-out;
    will-change: transform;
    backface-visibility: hidden;
    cursor: pointer;
    /* The card is nested one level inside its grid item (the <li>), so it needs
       an explicit height to pick up the row's stretched height -- otherwise it
       sizes to its own content and cards in a row come out uneven. Both pages'
       micro-templates wrap each card in an <li>, so this is load-bearing on
       both. */
    height: 100%;
  }

  .kb-update-card:hover {
    border-color: var(--neutral-border);
    box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
      var(--shadow-hover);
  }

  .kb-update-card:active {
    border-color: var(--neutral-border-strong);
    box-shadow: none;
  }

  /* Last of the three state rules, and it has to be: all three are (0,2,1), so
     source order is the only thing making the focus ring win on a card that is
     both hovered and focused. Same shape as .kb-section-card above, where the
     long version of the reasoning is. */
  .kb-update-card:focus-visible {
    --outline-offset: 2px;
    box-shadow: none;
    transition: none;
  }

  /* NO COLOUR CYCLE HERE, which is the deliberate difference from the home
     page's .kb-badge--0 .. --3. Over there the badge names the article's
     *category* and the colour is keyed to category identity, so four variants
     are meaningful. On the category page every article shares the one category
     by construction, so the badge names its *section* instead -- the more
     informative of the two there -- and a per-item colour would then be
     decorative rather than carrying information. So there is one treatment, and
     it is the home page's neutral --0: border, fill and text identical to the
     card the badge sits on.

     ONLY THE CATEGORY PAGE RENDERS A BADGE. The section page's article cards
     deliberately have none: every article on that page is in the section the
     <h1> already names, so the badge would read the same on all of them, which
     is the same argument this comment makes against badging by category on the
     category page. One line of markup in #tmpl-section-articles if that ever
     changes.

     Padding and line-height are the Codex Badge measured 1:1 -- 4px/6px and
     line-height 1, the same pair [class*='badge-'] carries further up this file
     with the measurement note. Both are FLAT on purpose: the inline padding was
     var(--space-2xs), a fluid clamp that grew to 12px at desktop against 2px of
     block padding, and a component's insides stay off the --space-* scale
     regardless. The home page's .kb-badge has the same two declarations, with
     the longer version of this note. Keep the two in step. */
  .kb-badge {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    padding: 0.25rem 0.375rem;
    border: var(--border-width-regular) solid var(--neutral-border-soft);
    border-radius: var(--radius-sm);
    background-color: var(--neutral-bg);
    color: var(--neutral-text);
    font-size: var(--step--1);
    font-weight: var(--font-bold);
    text-decoration: none;
    line-height: 1;
  }

  /* --step-1, where .kb-section-card-title above is --step-2. The cards are the
     same width but they are not the same weight of destination: a section card
     names one of a handful of topics, an update card names one article out of a
     section's forty. The trim is in the @supports block near the top of this
     file with the other three headings. */
  .kb-update-card-title {
    font-family: var(--ff-logo);
    font-weight: var(--font-bold);
    font-size: var(--step-1);
    line-height: var(--leading-heading-1);
    color: var(--neutral-text);
  }

  /* margin-top: auto is what keeps these links level across a row whatever
     number of lines the titles above them wrap to, now that the cards are
     stretched to a common height. --step--1 rather than --step-0 is Joy's call
     from 2026-10-21 on the home page: the link is the last thing in the stack
     and reads better staying quieter. */
  .kb-update-card-link {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    margin-top: auto;
    font-family: var(--brand-font-family-sans);
    font-weight: var(--font-bold);
    font-size: var(--step--1);
    line-height: var(--leading-control);
    color: var(--neutral-text);
  }

  /* Shared with :focus-visible, not only :hover. The card's ring says "you are
     here"; the underline says "this navigates" -- two different messages, and a
     keyboard user needs the second as much as a mouse user does. */
  .kb-update-card:is(:hover, :focus-visible) .kb-update-card-link {
    text-decoration: underline;
  }
}
/* !SECTION - Brand refresh: shared page components */

/* SECTION - Brand refresh: shared form controls
   ========================================================================
   The text field, the CKEditor shell and the file-upload box, shared by the
   request page's comment form and the new request page's ticket form. Joy asked
   for this on 2026-10-24 while the request page was being converted: "This is a
   good opportunity to put our form styles into play that we will also use on
   the new request page."

   CODEX HAS A FULL SET OF FORM ROLES AND WE ALREADY CARRY ALL ELEVEN OF THEM
   with both tiers (css-brand-tokens.css:471-481 light, :957-967 dark):
   --form-bg, -bg-hover, -bg-active, -bg-error, -bg-readonly, -bg-highlight,
   --form-border, -border-active, -border-error, -border-readonly and
   --form-focus-ring. Read out of the Input component's own variables (Figma
   node 6738:46804) they are #faffff / #f2fdfd / #effbfb fills against a #809094
   resting border going to #4f5f63 active, which is the whole of the control's
   colour story. Nothing here needs a neutral role, and nothing here needs a
   dark-mode rule.

   WHY A SHARED SECTION AND NOT TWO PAGE FILES. Both pages were styling the
   same three controls, separately, and neither was styling them the same way.
   The request page addressed CKEditor through three selector chains of the form
   `#main-content > section.request-main > form > div.comment-container >
   div.comment-fields > div.ck.ck-reset.ck-editor.ck-rounded-corners`, and the
   new request page through four more of the same shape rooted at
   `#new-request-form`. Seven chains, each one encoding the exact nesting of a
   DOM that Zendesk and CKEditor generate and can change under us, to say
   "the editor has a border". They are replaced here by the component's own
   class names, which is what CKEditor documents as stable.

   SCOPED TO THE TWO PAGE CLASSES, both for the reason the section above this
   one gives at length -- these names must not reach other pages -- and for a
   specificity reason particular to this block. css-page-new-request.css loads
   at document_head.hbs:226 and this file at :220, so an unscoped rule here
   would lose every tie to that page file. The `html.` prefix buys (0,1,1) of
   head room, which is enough for every rule below to win on specificity
   regardless of load order. The exception is `!important`, which specificity
   cannot beat -- see the upload box.

   `.form-field` IS DELIBERATELY ABSENT. It looks like it belongs here, but it
   is also on the header's search field (header.hbs:98) and on the two requests
   page filter controls (requests_page.hbs:75, :82), styled unscoped from
   css-page-new-request.css:106. Pulling its layout rules behind a page scope
   would quietly unstyle three controls on two other pages. It stays where it
   is until the new request page's round, which is where the Garden combobox,
   `.nesty-input` and the submit button chains are being left as well.
   ===================================================================== */

html.request-page,
html.new-request-page {
  /* ---- The control recipe ----
     Fixed rems rather than the fluid --space-* scale, on the same argument as
     .kb-icon-graphic's 3.75rem above: this is a component's internal padding,
     not page layout, and Codex states it as flat values -- `space/2-5` (10px)
     block, `space/3` (12px) inline. The fluid tokens would have taken the
     inline padding from 12px to 18px between a phone and a desktop, which
     makes the field look like a different control at the two ends.
     Declared as custom properties because four selectors below need the same
     three values and two of them need them inside an `!important`. */
  --kb-field-padding-block: 0.625rem;
  --kb-field-padding-inline: 0.75rem;
  --kb-field-radius: var(--radius-sm);

  /* ---- Text fields ----
     Two selectors, one control. `.comment-show-container` is the request
     page's collapsed "Add to conversation" input (request_page.hbs:90) and
     the Garden container id is every text field Zendesk renders into the new
     request form. They were 1px --clr-neutral-40 with a 0.25rem radius and an
     8px/25px padding on one page and a 1rem/1.5rem padding on the other.

     NO `transition-duration` WITHOUT A `transition-property`, which is what
     both pages' hover rules had -- `transition-duration: var(...)` on its own
     animates nothing, because the initial `transition-property` is `all` but
     the initial duration applies to it only once a shorthand or a property
     list exists. Stating the property here is what makes the hover actually
     ease rather than snap. */
  .comment-show-container,
  input[data-garden-container-id='containers.field.input'] {
    width: 100%;
    padding: var(--kb-field-padding-block) var(--kb-field-padding-inline);
    border: var(--border-width-regular) solid var(--form-border);
    border-radius: var(--kb-field-radius);
    background-color: var(--form-bg);
    color: var(--neutral-text);
    font-family: var(--brand-font-family-sans);
    font-size: var(--step-0);
    line-height: var(--leading-body-0);
    text-align: inherit;
    transition:
      border-color 150ms ease-in-out,
      background-color 150ms ease-in-out;
  }

  /* HOVER MOVES THE FILL AND NOT THE BORDER, corrected 2026-10-25 against the
     Input component's own state table (Figma node 19743:3116, read at native
     resolution and sampled pixel by pixel because that frame's child ids come
     back empty). Every one of the six states Codex draws is one of the roles
     already in css-brand-tokens.css, which is the strongest confirmation yet
     that the dark tier in that file is right:

       state      border                 fill
       default    --form-border          --form-bg        #809094 / #faffff
       hover      --form-border          --form-bg-hover  #809094 / #f2fdfd
       active     --form-border-active   --form-bg-active  #4f5f63 / #effbfb
       focus      --form-border-active   --form-bg-active  + the ring below
       invalid    --form-border-error    --form-bg-error  #f65154 / #fffafb
       readonly   --form-border-readonly --form-bg-readonly #bbcbce / #ebf8f8
       disabled   the default control at opacity 0.5

     The dark column of that frame reconciles to the dark tier the same way,
     once you allow for the field edges landing on half-pixels there: the
     border reads as a 50/50 blend over the fill, and un-blending it gives
     #698198 resting and #99aec1 active, both exact.

     THIS RULE USED TO SET --form-border-active ON HOVER, and the note that
     replaced said it was "the text field's hover border exactly" -- true, but
     both controls were wrong together. Codex holds the resting border through
     hover and spends only the fill, then spends the border on the states that
     mean the field is actually taking input. The combobox in
     css-page-new-request.css is corrected to match.
     JOY: this is the one change in this round you might want back. Hover is
     quieter than it was -- a fill shift rather than a border shift -- though it
     is a real shift in both tiers (#faffff to #f2fdfd light, #172029 to
     #10171e dark). Say so and it goes back to a border change.

     THE LAST TWO ROWS WERE WRONG HERE UNTIL 2026-10-25 and both corrections
     are Joy's. This note used to end "DISABLED IS NOT IMPLEMENTED and that is
     deliberate ... there are no disabled roles anywhere in it". Her reply:
     "There are disabled roles -- they are called ReadOnly." She is right, and
     the row was mislabelled rather than missing: --form-bg-readonly and
     --form-border-readonly are in the dump with both tiers
     (css-brand-tokens.css), and their light values are the ReadOnly row's
     exactly. Then: "In the examples file of Input documentation they also have
     a specific disabled state" -- also right, and it is a different row again,
     which is why one of the two looked unresolvable. Two states, not one
     misnamed state.

     AND THE DISABLED PAIR IS NOT A COLOUR AT ALL, which is why it matched no
     role: #b6c3c6 on #f3fbfc is the resting control at `opacity: 0.5` over the
     page. Solved by algebra rather than by hunting for a token -- with a page
     background P and the resting values B and F, a 50% composite reads
     0.5B + 0.5P and 0.5F + 0.5P, and all three channels of both samples fall
     out to within a digit in BOTH tiers. So Codex's disabled field needs no new
     token and no dark-mode rule; it needs the one declaration below.

     ALL FOUR STATE RULES ARE WRITTEN NOW even though nothing on either of these
     two forms is readonly or disabled today, because the cost is four small
     rules against the chance that a Zendesk ticket form turns a field readonly
     from the admin side and we find out from a screenshot. */
  .comment-show-container:hover,
  input[data-garden-container-id='containers.field.input']:hover {
    background-color: var(--form-bg-hover);
  }

  /* ---- Invalid ----
     Joy, 2026-10-25: "are you able to implement codex invalid states with the
     red backgrounds, borders, and box shadow?" Yes, and all three of them are
     roles we already carry.

     THE BOX SHADOW IS A NAMED EFFECT, not a value to read off a render. Figma's
     `glow/error` on the invalid field is a DROP_SHADOW with offset 0,0, spread
     0, radius bound to `glow-error-blur`, colour bound to `critical/glow`. Both
     of those are in the dump -- --glow-error-blur 12px, --critical-glow
     #fb808266 light and #9d070a8c dark -- so the shadow is written from the
     variables rather than from a measurement, and it is a halo round the
     resting invalid field in both tiers, not a focus effect.

     THERE IS NO ERROR VARIANT OF THE FOCUS RING, checked rather than assumed:
     Codex's FocusRing component (node 12267:2740) binds `form/focus-ring` at
     `border-width/heavy` and nothing else, with no status variants. So a
     focused invalid field keeps the red border and the red glow and takes the
     ordinary ring on top, which is what these selectors produce -- this rule is
     (0,3,2) scoped against the focus rule's (0,4,1), and an attribute outranks
     a pseudo-class, so the red border survives focus.

     THE HOOK IS GARDEN'S OWN `aria-invalid` AND IT IS LIVE ON THIS FORM, which
     took finding. assets/new-request-form-bundle.js passes `validation` to
     Field.Message and to nothing else, so reading only that bundle says this
     attribute never appears and a rule keyed on it is dead code. It is the
     wrong bundle: the ticket form's fields are rendered by
     assets/ticket-fields-bundle.js, which passes `validation: error ? "error" :
     undefined` to the Input, the Textarea, the Datepicker and the Combobox of
     every field it builds, and Garden's StyledTextInput sets
     `aria-invalid` from it (assets/shared-bundle.js). Same element our resting
     rule already names -- the container id and `forms.input` are both on it. */
  .comment-show-container[aria-invalid='true'],
  input[data-garden-container-id='containers.field.input'][aria-invalid='true'] {
    border-color: var(--form-border-error);
    background-color: var(--form-bg-error);
    box-shadow: 0 0 var(--glow-error-blur) 0 var(--critical-glow);
  }

  /* ---- ReadOnly ----
     Codex's own row, now that it has its name back. The ink moves as well as
     the fill and the border: --neutral-text-soft, because a field you cannot
     type in should not present its contents as editable text. `readonly` is the
     attribute Zendesk would set, and it is not the same as `disabled` -- a
     readonly field still focuses, still takes a ring, and still submits. */
  .comment-show-container[readonly],
  input[data-garden-container-id='containers.field.input'][readonly] {
    border-color: var(--form-border-readonly);
    background-color: var(--form-bg-readonly);
    color: var(--neutral-text-soft);
  }

  /* ---- Disabled ----
     One declaration, for the reason the hover note above works out: Codex's
     disabled field is the resting field at half opacity, not a field with its
     own two colours. `:disabled` rather than `[disabled]` so it also catches a
     field disabled by a fieldset, and `cursor` because the pointer is the only
     other thing that tells you the control is out of play. */
  .comment-show-container:disabled,
  input[data-garden-container-id='containers.field.input']:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* THE RING IS NOT STATED HERE, and that is the point. Both pages drew their
     own focus state -- `border: 2px solid var(--clr-primary-100)` plus
     `box-shadow: var(--box-shadow)`, which is an orange 2px halo built out of
     --clr-primary-10. css-reset.css:19 already gives every focusable element
     on the site a --form-focus-ring outline on :focus-visible, so a component
     that wants a tighter ring pins the offset and nothing else, exactly as
     .kb-section-card and .collapsible-sidebar-toggle do.
     --form-bg-active is Codex's own focused fill. In dark mode it is
     deliberately the same value as --form-bg (css-brand-tokens.css:846
     explains why), so there the ring and the border carry the state alone.

     THE 2px OFFSET IS NOT A GUESS. Codex's focused field is a 1px
     --form-border-active with a 2px --form-focus-ring two pixels outside it,
     and that frame renders it crisply enough to count: at the focused row the
     pixels run page-bg, ring, ring, page-bg, page-bg, border, fill. So
     `outline: 2px solid var(--form-focus-ring)` at `outline-offset: 2px`,
     which is exactly what the sitewide rule already draws.

     `border-color` IS STATED NOW, added 2026-10-25 from the same table. It was
     left out on the argument that restating a border shifts the control by 1px
     -- but that is only true of restating `border-width`, which the two old
     rules did and this does not. Without it a focused field kept its resting
     border, which is the one place our control and Codex's disagreed in a way
     a keyboard user sees. */
  .comment-show-container:focus-visible,
  input[data-garden-container-id='containers.field.input']:focus-visible {
    --outline-offset: 2px;
    border-color: var(--form-border-active);
    background-color: var(--form-bg-active);
  }

  .comment-show-container::placeholder,
  input[data-garden-container-id='containers.field.input']::placeholder {
    color: var(--neutral-text-soft);
  }

  /* ---- CKEditor ----
     CKEditor 5 themes itself through its own custom properties, which is the
     documented and version-stable way in: setting --ck-color-base-border here
     reaches the shell, the toolbar, the dropdown panels and the balloon
     without this file knowing any of their class names. A name CKEditor does
     not use is simply an unused declaration, so the structural rules that
     follow carry the look on their own if any of these are wrong -- they are
     belt to those braces, not a substitute for them.

     FOR THE TOOLBAR ITEMS THE VARIABLES ARE NOT BELT AND BRACES, THEY ARE THE
     ONLY WAY IN, which is the finding that reopened this block on 2026-10-25.
     Joy: "On hover these elements should have the same treatment as the
     datatable column headers where the background changes -- the darkening on
     hover is too dark" (dark mode), "or to make things simpler you can just
     have a light border around each element (like you have the dark border
     around the elements on hover in light mode)."

     She had read the mechanism correctly from the symptom: it IS a dark border,
     and it is the same border in both tiers. assets/wysiwyg-bundle.js carries a
     SECOND hand-written CSS block, separate from the "KB4 Brand Overrides" one
     this branch deleted (see the note on :focus-within below) and this one is
     Zendesk's -- a CKEditor-to-Garden bridge, several hundred lines, starting at
     raw offset ~449k. It is left in place, because unlike the KB4 block it is
     doing real work. Four of its rules decide this control:

       .ck.ck-button:not(.ck-on):not([role="menuitemradio"])
         { color: var(--ck-color-button-default-color) }          (0,4,0)
       .ck.ck-button.ck-off:not([role="menuitemradio"]):not(.ck-disabled):hover
         { border-color: ...off-hover-border; color: ...off-hover-color }
                                                                  (0,6,0)
       .ck.ck-button.ck-on:not([role="menuitemradio"])
         { border-color: var(--ck-color-button-on-border) }        (0,4,0)
       .ck.ck-toolbar .ck.ck-toolbar__separator
         { background: var(--zd-color-grey-300) }                 (0,4,0)

     and it feeds them from `--zd-color-grey-*` primitives on `:root` -- 700
     #5c6970 for the resting ink, 800 #39434b for the hover border and hover
     ink. Those are Zendesk's frozen Garden primitives: one value, no dark tier,
     exactly the defect this branch exists to fix, and the reason a dark-mode
     toolbar item gets a near-black ring drawn round it on hover.

     THE RESTING INK WAS NEVER THE PROBLEM AND THE HOVER INK ALWAYS WAS, which
     the specificity numbers say exactly. `.ck.ck-editor__top .ck-button` below
     is (0,4,1) once this section's scope is applied, so it beats the bridge's
     resting rule at (0,4,0) on the type selector alone -- the toolbar has been
     --neutral-icon all along. But `.ck.ck-editor__top .ck-button:hover` is
     (0,5,1) against that hover rule's (0,6,0), so on hover the ink went
     #39434b as well as the border: in dark mode a near-black glyph inside a
     near-black ring, on a #172029 strip. Both halves of what Joy saw, from one
     rule.

     ESCALATING OUR SIDE IS NOT THE FIX. There are four rules to chase, in a
     file we do not own, and the next Zendesk bundle update moves them. Setting
     the variables they read is: it is CKEditor's documented theming surface, it
     is one place, and it works WITH the bridge instead of against it. The rules
     below stay as the structural half and as the resting colour, which they
     already won; the states now arrive here.

     `:root` IS WHERE THE BRIDGE DECLARES THESE and this rule is on a class, so
     ours win inside the editor and inherit down to every button in it. The
     panels and balloons CKEditor appends to <body> are `.ck-reset_all`, which
     is why that selector is on this rule as well -- without it a dropdown list
     would still be reading Zendesk's greys.

     THE RAMP IS THE DATATABLE'S, which is what Joy asked for first and is a
     better answer than the border fallback she offered second: component
     --neutral-bg-component-hover on hover and -active on press (#d4e2e4 /
     #c9d7d9 light, #3f5467 / #536a80 dark), with every border token in the set
     turned transparent so nothing rings the item at all. It is the same ramp
     the requests page's DataTable column headers take
     (css-page-requests.css:730), so the two controls now agree, and in dark
     mode it reads as a lightening rather than a darkening -- which is the
     direction a hover on a dark surface has to move.

     --ck-color-button-default-hover-background WAS --neutral-bg-soft, which is
     the toolbar's own fill: in dark mode that is #172029 on #172029, so there
     was no hover fill at all and the ring was the whole of the state. The
     active fill was --neutral-bg-highlight for the same not-quite-reason.

     THE SPLIT-BUTTON AND TOGGLE BORDERS NEED NAMING SEPARATELY even though
     "transparent" is the answer for all of them. The bridge draws the arrow
     segment's border from --ck-color-button-on-hover-border and the pressed
     toggle's from --ck-color-button-on-border, both of which resolve to
     grey-900 and neither of which is reached by setting the `off` pair. */
  .ck.ck-editor,
  .ck.ck-reset_all {
    --ck-border-radius: var(--kb-field-radius);
    --ck-color-base-border: var(--neutral-border-soft);
    --ck-color-base-background: var(--form-bg);
    --ck-color-base-text: var(--neutral-text);
    --ck-color-text: var(--neutral-text);
    --ck-color-focus-border: var(--form-border-active);
    --ck-color-toolbar-background: var(--neutral-bg-soft);
    --ck-color-toolbar-border: var(--neutral-border-soft);

    /* Toolbar items: the ink, then the two surfaces, then every border the
       bridge can draw round one. */
    --ck-color-button-default-color: var(--neutral-icon);
    --ck-color-button-default-background: transparent;
    --ck-color-button-default-hover-background: var(
      --neutral-bg-component-hover
    );
    --ck-color-button-default-active-background: var(
      --neutral-bg-component-active
    );
    --ck-color-button-off-hover-color: var(--neutral-icon-strong);
    --ck-color-button-off-hover-border: transparent;
    --ck-color-button-on-background: transparent;
    --ck-color-button-on-color: var(--brand-icon);
    --ck-color-button-on-border: transparent;
    --ck-color-button-on-hover-background: var(--neutral-bg-component-hover);
    --ck-color-button-on-hover-border: transparent;
    --ck-color-button-on-active-background: var(--neutral-bg-component-active);
    --ck-color-split-button-hover-background: var(--neutral-bg-component-hover);
    --ck-color-split-button-hover-border: transparent;

    --ck-color-shadow-drop: var(--shadow-regular);
  }

  /* The shell. Both pages drew this border on a six- or seven-step child
     selector; `.ck.ck-editor` is (0,2,0) and scoped it is (0,3,1), which beats
     CKEditor's own `.ck-rounded-corners .ck.ck-editor__top .ck-sticky-panel
     .ck-sticky-panel__content` at (0,4,0) without needing to mirror its
     nesting.

     --neutral-border-soft, NOT --form-border, as of 2026-10-25. Joy asked for
     Codex's Editor component to be implemented (Figma node 21140:2410) and
     this is where it differs from the text field above: the editor is drawn
     with the softer neutral border, not the form border. Measured rather than
     read off a layer name, because that frame's border lands on a half-pixel
     and reads as a blend -- un-blending the two columns at the top edge gives
     #c9d6d9 in light and #2d3b47 in dark against --neutral-border-soft's
     #c9d7d9 / #2e3e4e, so the role is not in doubt.

     CODEX'S EDITOR IS QUILL, NOT CKEDITOR, and that is worth knowing before
     reading any further: `editor-toolbar-item` and the `Normal`/`Sans Serif`
     dropdowns in that frame are Quill's. The visual treatment transfers
     cleanly -- it is a tinted toolbar over a plain editable, which is what
     every one of these rules builds -- but none of its markup or class names
     do, so everything here stays on CKEditor's own documented hooks.

     THERE IS NO margin-block-end HERE ANY MORE, removed 2026-10-25. Joy, on a
     form submitted with the fields empty: "there is a space about
     'Description...'". There was, and it was this -- `margin-block-end:
     var(--space-2xs)`, 12px at her viewport, sitting inside the field's flex
     column between the editor and its error message. Measured rather than
     assumed: the control-to-message gaps in her light screenshot run 15 device
     pixels on Subject, 21 on Country and 39 on Description, and 39 - 15 is the
     12 this declaration was adding. Zendesk already spaces both sides of this
     control from assets/ticket-fields-bundle.js, where the Description field is
     a styled Garden Field carrying `.ck.ck-editor { margin-top: space.xs }` and
     its message a styled Message carrying `.ck.ck-editor + & { margin-top:
     space.xs }`. So the gap was never missing; it was being paid twice, and the
     half we were paying was the half nothing else on the form paid. */
  .ck.ck-editor {
    border: var(--border-width-regular) solid var(--neutral-border-soft);
    border-radius: var(--kb-field-radius);
    background-color: var(--form-bg);
    transition:
      border-color 150ms ease-in-out,
      background-color 150ms ease-in-out,
      box-shadow 150ms ease-in-out;
  }

  .ck.ck-editor:hover {
    border-color: var(--form-border-active);
  }

  /* The ring goes on the shell, not on the editable area inside it, so the
     whole control reads as focused -- and `:focus-within` rather than
     `:focus-visible`, because the element that actually takes focus is the
     contenteditable div two levels down. The two old rules put a 2px orange
     border here and then CKEditor's own `.ck-focused` put another one on the
     editable, so a focused editor had two competing outlines.

     THIS RULE WAS BEING BEATEN AND THE CULPRIT WAS OURS. Joy, 2026-10-25:
     "focus:visible on the Description field is still has a holdover of old
     focus styles." It was -- an orange 2px border with a pale orange halo,
     the pre-refresh treatment exactly -- and nothing in any stylesheet in this
     theme was drawing it. It came out of assets/wysiwyg-bundle.js, where a
     block headed "===== KB4 Brand Overrides =====" had been hand-appended
     inside one of webpack's CSS-module template literals. (Written without its
     comment delimiters on purpose: a `*` followed by a `/` inside a CSS
     comment ends the comment there and silently deletes whatever rule comes
     next, which is a hazard this theme has been bitten by before.) The
     block set
     --ck-color-focus-border: #ef4623 and --ck-color-focus-outer-shadow:
     #facac0, and then, decisively:

       .ck.ck-reset.ck-editor.ck-rounded-corners:focus-within {
         border: 2px solid #ef4623;
         box-shadow: 0px 0px 0px 2px #facac0;
       }

     That selector is (0,5,0). This rule, scoped, is (0,4,1). Specificity, not
     load order -- so escalating our side could never have been the fix, and
     adding `!important` would have been fighting our own file.

     IT IS DELETED, which the branch could afford for three reasons worth
     recording: everything in the block is now stated here with roles and both
     tiers, the bundle is a vendored artifact with no build behind it (no src/,
     no webpack config, the only package.json scripts are prettier, and
     `git log` on the file shows one commit -- the initial import), and `node
     --check` passes on the result. Three other things went with it, each of
     which the branch wanted gone anyway:

       - `.ck.ck-button:focus-visible { outline-width: 0 !important; box-shadow:
         none !important; }`, under a comment reading "KB4 Focus-Visible
         Restore", which did the exact opposite of its name and left the
         toolbar with no visible keyboard focus at all. Codex's own editor
         draws a ring on every toolbar item -- see the --brand-icon note
         further down -- so this deletion is required by the design, not just
         tolerated by it.
       - Single-value #c2c2c2 and #333333 borders on the shell and the toolbar
         separator, frozen light, the same defect this branch exists to fix.
       - Two `textarea[aria-invalid="true"]` rules that forced the toolbar
         separator back to neutral #c2c2c2, which defeated the editor's error
         state outright.

     After the deletion, `#ef4623` does not appear anywhere in the bundle. */
  .ck.ck-editor:focus-within {
    outline: var(--outline-width, 2px) solid var(--form-focus-ring);
    outline-offset: 2px;
    border-color: var(--form-border-active);
  }

  /* ---- The editor's invalid state ----
     Joy, on a form submitted empty: "there is a red border that appears within
     the text editor", in both tiers. There is, and it is a stray hairline
     rather than an error state. The same Zendesk bridge block in
     assets/wysiwyg-bundle.js that owns the toolbar colours also owns this:

       textarea[aria-invalid="true"] + .ck.ck-editor .ck-sticky-panel__content,
       textarea[aria-invalid="true"] + .ck.ck-editor
         .ck-editor__editable:not(.ck-focused) { border-color: #cc3340
         !important }

     In stock Zendesk that is right, because there the toolbar strip and the
     editable are two separately bordered boxes and reddening both outlines the
     control. In our design they are one shell with one border and a single
     divider under the toolbar, so the editable half lands on a border we have
     already set to `border: 0` (our width and style win; only the colour is
     theirs, so nothing paints) and the toolbar half recolours the divider --
     which is a red line drawn *across* the inside of the editor, exactly what
     she photographed.

     THE TOOLBAR HALF IS A SPECIFICITY TIE AND LOAD ORDER WAS DECIDING IT. Our
     `.ck-sticky-panel__content` rule below is (0,4,1) with `!important` once
     this section's scope is applied, and the bridge's selector is also (0,4,1)
     with `!important` -- and bundle CSS is injected into <head> at runtime,
     after every sheet we control, so the bridge won. Naming the same attribute
     the bridge names, under the page scope, takes this rule to (0,5,2) and the
     tie disappears. It keeps the `!important`, and it has to: importance is
     settled before specificity, so a quieter rule at any specificity would
     still lose to theirs. What the extra attribute buys is the ordering between
     two `!important` declarations, which is decided by specificity first and
     only falls through to load order on a tie -- and load order is the one
     contest a stylesheet cannot win against a bundle.

     SO THE ERROR TREATMENT MOVES TO THE SHELL, which is where Codex puts it:
     --form-border-error round the whole control, --form-bg-error under the
     editable, and the `glow/error` drop shadow. This is Joy's "are you able to
     implement codex invalid states with the red backgrounds, borders, and box
     shadow?" applied to the editor; the text field and the combobox take the
     same three roles, and the long note on the text field has the measurements.

     THE DIVIDER STAYS NEUTRAL in the error state. It is structure -- it is what
     separates the two surfaces of the control -- not a status edge, and the
     shell's border is already saying everything red has to say. */
  textarea[aria-invalid='true'] + .ck.ck-editor {
    border-color: var(--form-border-error);
    background-color: var(--form-bg-error);
    box-shadow: 0 0 var(--glow-error-blur) 0 var(--critical-glow);
  }

  textarea[aria-invalid='true'] + .ck.ck-editor .ck-sticky-panel__content {
    border-block-end-color: var(--neutral-border-soft) !important;
  }

  /* The editable area and the toolbar. CKEditor gives both their own 1px
     border and their own radius, which inside our shell reads as a box in a
     box; `border: 0` on the editable and a single top hairline under the
     toolbar is the treatment both pages had been reaching for through their
     chains. !important on the editable's border because CKEditor's
     `.ck.ck-editor__main > .ck-editor__editable:not(.ck-focused)` and
     `.ck-focused` rules both set it from its own stylesheet, which loads from
     Zendesk's CDN after all of ours.

     !important ON THE COLOUR FOR THE SAME REASON, added 2026-10-24 after Joy
     previewed this: "In dark mode the input text needs to be the dark mode
     theme color." It does win -- her console reads rgb(255, 255, 255) here on
     the dark page -- but on its own it did not fix anything she could see,
     because the text she was looking at is in a <p> inside this element and
     that <p> is coloured separately. The rule below is the one that fixed it,
     and it is worth reading the two together.

     What this !important is still for: CKEditor's own `color` on this element,
     from the CDN sheet, which would otherwise set the colour the content
     inherits. Keeping it means the rule below can say `inherit` and have one
     place to change.

     --ck-color-base-text and --ck-color-text are set on .ck.ck-editor above
     and are NOT what fixes either half. They are the right surface for
     CKEditor's own chrome, which reads them, but a custom property cannot
     outrank a direct `color` declaration on an element -- it only feeds one. */
  .ck.ck-editor__main > .ck-editor__editable {
    padding: var(--kb-field-padding-block) var(--kb-field-padding-inline);
    border: 0 !important;
    border-radius: 0 !important;
    background-color: transparent;
    box-shadow: none !important;
    color: var(--neutral-text) !important;
    font-family: var(--brand-font-family-sans);
    font-size: var(--step-0);
    line-height: var(--leading-body-0);
  }

  /* THE TEXT IS IN THE <p>, NOT ON THE EDITABLE, and the two are coloured by
     different things. Joy's console on 2026-10-24, on the dark page:

       editable class: ck-blurred ck ck-content ck-editor__editable
                       ck-rounded-corners ck-editor__editable_inline
       editable color: rgb(255, 255, 255)   <- the rule above, working
       --neutral-text here: #ffffff         <- the token, correct
       p color: rgb(0, 0, 0)   inline: null <- what she could actually see

     So the !important above was never the problem and the tier was never the
     problem. The paragraph CKEditor puts the caret in was coming out pure
     black -- which is not a value in this theme at all; --neutral-text is
     #10171e in light and #ffffff in dark, and nothing of ours resolves to
     #000. It is !important, or it would have lost to the (0,3,1) this rule
     already had. Hence the !important here: it is the second one, on the
     element that actually holds the text, and it is the one that fixes the
     symptom.

     IT IS THE ZENDESK BRIDGE BLOCK, pinned down on 2026-10-25 while the
     toolbar's colours were being traced -- this note said "CKEditor's, from its
     CDN sheet" on the reasonable assumption that a hardcoded black inside an
     editor was the editor's. It is not; it is
     `.ck.ck-content p, .ck.ck-content ul, .ck.ck-content ol { color: #000
     !important }`, hand-written in assets/wysiwyg-bundle.js alongside the
     toolbar rules the variables block above documents. That is (0,2,0), so this
     rule wins the !important contest on specificity. Worth knowing because it
     is the third defect this round that traced back to the same block, and
     because it means the fix does not depend on anything Zendesk serves from a
     CDN.

     `inherit` rather than the token restated, so the editable above stays the
     single place the colour is set. The negated list keeps anything with a
     deliberate colour of its own: links keep the site link colour, and
     <code>/<pre>/<kbd>/<samp>/<mark> keep CKEditor's. Everything else in a
     comment is text that should be the tier's ink.

     THE SELECTOR IS A UNIVERSAL ONE NOW because the element list was a guess
     and the readout showed it did not need to be. `.ck.ck-content` is the
     editable itself -- it carries both classes -- and doubling up is what
     gets this to (0,2,0) before the scope. :where() contributes nothing, so
     wrapping `*:not(...)` in it keeps the whole thing at (0,3,1) rather than
     letting the :not() raise it; the number cannot drift as that list grows. */
  .ck.ck-content :where(*:not(a, code, pre, kbd, samp, mark)) {
    color: inherit !important;
  }

  /* THE TOOLBAR IS A TINTED STRIP, NOT A TRANSPARENT ONE, as of 2026-10-25.
     This was the other half of Joy's Editor ask. Codex's editor is two
     surfaces, not one: --neutral-bg-soft across the toolbar and --form-bg
     under the editable, divided by a --neutral-border-soft hairline. Sampled
     dead-on in light -- #ebf8f8 above the rule, #faffff below it -- and the
     reason the dark render looks like one flat surface is that
     --neutral-bg-soft and --form-bg are BOTH #172029 in dark. So one rule set
     serves both tiers and there is no dark-mode rule to write; what changes
     between them is only whether you can see the division, and the hairline
     is there either way.

     THE TOP CORNERS HAVE TO COME BACK with the tint. `border-radius: 0` was
     harmless while this strip was transparent -- the shell's own 6px corners
     were all you could see. A filled strip with square corners would square
     off the top of the shell. The inner radius is the outer radius less the
     border it sits inside, which is what keeps the tint from showing a sliver
     outside the curve. */
  .ck.ck-editor__top .ck-sticky-panel__content {
    border: 0 !important;
    border-block-end: var(--border-width-regular) solid
      var(--neutral-border-soft) !important;
    border-radius: calc(var(--kb-field-radius) - var(--border-width-regular))
      calc(var(--kb-field-radius) - var(--border-width-regular)) 0 0 !important;
    background-color: var(--neutral-bg-soft);
    box-shadow: none !important;
  }

  /* The toolbar itself stays transparent so the strip above is the only thing
     painting that surface -- two fills on two nested elements is how the
     corner problem gets hard to see. */
  .ck.ck-editor__top .ck.ck-toolbar {
    border: 0;
    background-color: transparent;
  }

  /* The group separator, found while tracing the hover border and not reported
     -- the Zendesk bridge paints it `var(--zd-color-grey-300)`, another of
     Garden's frozen single-value primitives, so a dark-mode toolbar got a pale
     grey divider between its button groups. --neutral-border-soft is the same
     role the divider under the toolbar takes, which is what it is: the same
     kind of line, turned ninety degrees. (0,4,1) against the bridge's (0,4,0),
     so no `!important`. */
  .ck.ck-editor__top .ck.ck-toolbar .ck.ck-toolbar__separator {
    background: var(--neutral-border-soft);
  }

  /* The toolbar's own buttons and icons. Both pages set this with a `*`
     selector under a long chain and only set colour and font; the icons are
     CKEditor's inline SVGs and take `color` through `currentColor`.

     --neutral-icon was already right. Codex's `editor-toolbar-item` has four
     states and three of them are icon roles we carry, which is the second time
     this round the design turned out to be the token file read back:

       default  --neutral-icon         #384649 / #b2c3d2
       hover    --neutral-icon-strong  #10171e / #ffffff
       active   --brand-icon           #ef4523 in BOTH tiers
       focus    a --form-focus-ring ring around the item

     This rule is the resting one and it wins -- (0,4,1) scoped, against the
     Zendesk bridge's (0,4,0). The HOVER rule below does not, and is kept for a
     different reason; its own note explains.

     THE FOCUS ROW DOES NEED A RULE, corrected 2026-10-25. This note used to say
     it did not, because css-reset.css's sitewide `*:focus-visible` draws that
     ring sitewide -- but `*:focus-visible` is (0,0,0), and the bridge block
     carries

       .ck.ck-button:focus-visible, .ck.ck-toolbar:focus-visible,
       .ck.ck-content.ck-editor__editable.ck-focused { box-shadow: 0 0 0 1px
         #fff, 0 0 0 3px #2f3941; outline: 2px solid transparent }

     at (0,3,0), which blanks the outline and replaces it with a hardcoded
     #2f3941 shadow ring on a hardcoded #fff spacer. Two frozen single-value
     colours, so a keyboard user tabbing the toolbar in dark mode gets a white
     halo and a near-black ring. Deleting the KB4 block was necessary for this
     row and it was not sufficient; this rule is the rest of it. The ring is
     stated as a box-shadow rather than an outline because that is the property
     the bridge actually paints -- restoring the outline would leave its shadow
     underneath, and two rings is what this control had before. Same geometry as
     the bridge's, same 1px spacer, the theme's two roles instead of its two
     hexes. */
  .ck.ck-editor__top .ck-button {
    color: var(--neutral-icon);
    font-family: var(--brand-font-family-sans);
    font-size: var(--step--1);
    transition: color var(--motion-duration-control) var(--ease-in-out);
  }

  .ck.ck-editor__top .ck-button:focus-visible {
    box-shadow:
      0 0 0 1px var(--neutral-bg-soft),
      0 0 0 3px var(--form-focus-ring);
  }

  /* THIS RULE LOSES AND IS KEPT ANYWAY. It is (0,5,1) against the bridge's
     `.ck.ck-button.ck-off:not([role="menuitemradio"]):not(.ck-disabled):hover`
     at (0,6,0), so --neutral-icon-strong arrives through
     --ck-color-button-off-hover-color in the variables block above, not from
     here. What this rule still does is cover the toolbar items that are not
     `.ck-off` -- the dropdown buttons and the split buttons' action halves,
     which the bridge's hover rule never reaches at all -- so without it those
     would hold --neutral-icon through hover while their neighbours lifted. */
  .ck.ck-editor__top .ck-button:hover {
    color: var(--neutral-icon-strong);
  }

  /* `.ck-on` is CKEditor's name for a toolbar item whose format is applied at
     the caret -- bold inside bold text. It is the state Codex paints orange,
     and the only place brand colour belongs anywhere in this control.
     --ck-color-button-on-background is set to transparent alongside it: the
     colour alone is the signal in Codex, where a filled pill plus orange ink
     would read as two states at once. */
  .ck.ck-editor__top .ck-button.ck-on {
    color: var(--brand-icon);
  }

  /* ---- The dropdown carets ----
     Joy, 2026-10-25: "in dark mode the dropdown carets are still dark". Only
     the carets -- every other toolbar glyph followed --neutral-icon -- and the
     reason is the one rule in the Zendesk bridge block that is not about
     colour at all:

       .ck.ck-icon.ck-icon_inherit-color :not([fill]) { fill: inherit }

     under a comment reading "Prevent issue where some Zendesk Garden icons are
     completely filled with currentColor, making the icon itself invisible."
     That is a real problem and the rule is a reasonable fix for it: Garden's
     icons are outline drawings whose <svg> root carries `fill: none`, so
     CKEditor's own `fill: currentColor` on their paths floods them into a solid
     blob, and `inherit` hands them the root's `none` back.

     THE TWO RULES ARE THE SAME SPECIFICITY and the bundle loads later.
     CKEditor's is `.ck.ck-icon.ck-icon_inherit-color *:not([fill])`, the
     bridge's is the descendant form above; both are (0,4,0), so the bridge
     wins and `fill: inherit` applies to every icon in the editor, not only the
     Garden ones. For an icon CKEditor draws itself that is wrong in the other
     direction: its arrow is a filled path with no `fill` attribute, and its
     <svg> root declares no fill either, so `inherit` walks up to the initial
     value and the caret paints black. Every tier, every theme -- it only shows
     in dark mode because black on #172029 is where it disappears.

     SO GIVE THE ARROW'S ROOT A FILL TO INHERIT, which is the narrow fix and
     keeps the bridge's Garden protection completely intact: this says nothing
     about any icon except the two arrows, and it sets the property on the root
     rather than on the paths, so the mechanism the bridge comment describes is
     the mechanism that carries the colour. `currentcolor` resolves to the
     button's own `color`, so the carets now follow --neutral-icon,
     --neutral-icon-strong and --brand-icon with everything else.

     TWO SELECTORS FOR TWO COMPONENTS, read out of the bundle rather than
     guessed. DropdownView's arrow is an IconView whose own element takes the
     class, so `.ck-dropdown__arrow` IS the <svg>. SplitButtonView's arrow is a
     ButtonView, so `.ck-splitbutton__arrow` is a <button> and the <svg> is the
     `.ck-icon` inside it. Not scoped to `.ck-editor__top`, because CKEditor
     appends its dropdown panels to <body> and nested list arrows live there. */
  .ck-dropdown__arrow,
  .ck-splitbutton__arrow .ck-icon {
    fill: currentcolor;
  }

  /* ---- The WYSIWYG swap ----
     Joy, 2026-10-24: "when I click into the field ... there is a flash before
     the WYSIWYG loads."

     WHAT THE FLASH IS. `{{wysiwyg 'body'}}` renders a plain
     `<textarea rows='7' data-helper='wysiwyg'>` and Zendesk's hosted script
     then builds CKEditor around it and hides it. Between those two moments the
     browser paints the raw textarea: a UA-default box, UA-default font, square
     corners, no toolbar. Nothing of ours is involved, which is why it could
     not be fixed by changing our own JS -- and the script is Zendesk's, so it
     cannot be deferred or reordered either.

     SO THE FIX IS TO MAKE THE TWO LOOK THE SAME rather than to hide one of
     them. The textarea takes the shell's border, radius, fill, font and
     padding, so what paints first is already the control the user is about to
     get and the swap stops being visible. Hiding it instead -- `visibility:
     hidden` until `.ck` exists -- would trade a flash for an empty gap, and
     would leave anyone whose editor fails to load with a form they cannot
     type in at all. This way the textarea is a working plain-text field in
     that case, which is what it is for.

     Styling the element Zendesk owns is safe in a way that scripting it is
     not: when CKEditor mounts it sets `display: none` on the textarea itself,
     and a display of none wins over everything here. */
  textarea[data-helper='wysiwyg'] {
    width: 100%;
    padding: var(--kb-field-padding-block) var(--kb-field-padding-inline);
    border: var(--border-width-regular) solid var(--form-border);
    border-radius: var(--kb-field-radius);
    background-color: var(--form-bg);
    color: var(--neutral-text);
    font-family: var(--brand-font-family-sans);
    font-size: var(--step-0);
    line-height: var(--leading-body-0);
    resize: vertical;
  }

  /* ---- The file upload box ----
     THIS ONE HAS A REAL CODEX COMPONENT AND IT IS NOT A FORM CONTROL. Joy,
     2026-10-24: "The figma FileUpload component should give you everything you
     need for the file upload part." It does, and it disagrees with the first
     pass at this block, which dressed the box as a field: --form-border,
     --form-bg, --radius-sm. Codex's `fileUpload` dropZone (node 505:29350,
     state=default) is built out of neutral roles instead --
     1px dashed neutral/border-strong, a neutral/bg-component-soft fill,
     radius/md, space/6 of padding, contents centred -- because it is a drop
     target, which is a surface you aim at, not a box you type in. Hence the
     heavier 12px radius and the much darker border: #384649 against the
     field's #809094.

     TWO SELECTORS BECAUSE ZENDESK RENDERS `{{upload}}` TWO WAYS. The Garden id
     is the current component; `.upload-dropzone` / `#upload-dropzone` is the
     older markup the new request page was still being styled for
     (css-page-new-request.css:220, legacy --clr tokens, now deleted). Which
     one a given form gets is Zendesk's call and not something a theme can
     read, so both names get the same recipe and whichever renders is right.

     THE dragOver STATE IS MAPPED TO :hover AND :focus-within, not to the real
     thing. Codex's second variant -- focus-ring border, bg-component-soft-hover
     fill, the prompt at 50% opacity -- is the state while a dragged file is
     over the box, and neither Garden nor Zendesk exposes a class or attribute
     for that moment that a stylesheet can hook. `:hover` is the honest
     approximation: it fires while a file is dragged across the box as well as
     while the pointer is, so the box does light up at the right time; it just
     also lights up when you are only passing over it.

     THE !important IS LOAD-BEARING HERE and is not the usual kind. Garden is
     CSS-in-JS: it emits a generated class per instance into a <style> element
     in <head> at runtime, so its declarations arrive after every stylesheet we
     control no matter where ours load. Specificity does not help, because the
     generated selector is a single class like ours. The rules this replaces
     (css-page-new-request.css:324) carried !important for the same reason, so
     this is the existing answer rather than a new one -- and it is why those
     rules had to be deleted rather than left: two !important rules on the same
     property are decided by load order, and the page file would have won.

     `text-align: center` CARRIES THE CENTRING ON ITS OWN, where Codex uses a
     centred flex column. The difference is deliberate: the Figma frame's
     dropZone holds nothing but the prompt, while the live box's children are
     Zendesk's and include the hidden <input type=file> and, depending on the
     render, the attached-file list. Centring the text gets the look without
     this file asserting anything about a child list it has not seen. */
  .upload-dropzone,
  [data-garden-id='forms.file_upload'] {
    padding: 1.5rem !important;
    border: var(--border-width-regular) dashed var(--neutral-border-strong) !important;
    border-radius: var(--radius-md) !important;
    background-color: var(--neutral-bg-component-soft) !important;
    color: var(--neutral-text) !important;
    font-size: var(--step-0);
    line-height: var(--leading-body-0);
    text-align: center;
    transition:
      border-color 150ms ease-in-out,
      background-color 150ms ease-in-out !important;
  }

  .upload-dropzone:hover,
  [data-garden-id='forms.file_upload']:hover {
    border-color: var(--form-focus-ring) !important;
    background-color: var(--neutral-bg-component-soft-hover) !important;
  }

  /* `:focus-within` and Garden's own `[aria-disabled='false']:focus`, which is
     the state it puts on the box when the hidden <input type=file> inside it
     takes focus. The old rule drew an orange border plus the --box-shadow
     halo; the ring comes from css-reset.css here as everywhere else, and
     `outline` needs no !important because Garden does not set one. */
  .upload-dropzone:focus-within,
  [data-garden-id='forms.file_upload']:focus-within,
  [data-garden-id='forms.file_upload'][aria-disabled='false']:focus {
    --outline-offset: 2px;
    border-color: var(--form-focus-ring) !important;
    background-color: var(--neutral-bg-component-soft-hover) !important;
  }

  /* THE PROMPT, AND WHY IT NEEDS A RULE OF ITS OWN. Joy, 2026-10-24: "We need
     to update the styles in the file upload box as well", with a light-mode
     capture in which "Add file or drop files here" is a washed-out grey on the
     pale fill -- close to unreadable, and the one thing in the box that is
     actually wrong. The box already carries `color: var(--neutral-text)
     !important`, so the colour was reaching the box and not the words: the
     prompt is Garden's own markup, a <span> and an <a> inside the dropzone,
     and Garden colours them itself. `inherit` on every descendant is what
     hands them the box's colour, and it has to be !important for the reason
     the box's own declarations do -- Garden's generated class arrives in a
     runtime <style> after every sheet we control, and its selector is a single
     class, so specificity is no help.

     The universal selector is deliberate and is safe because it is confined to
     the dropzone, whose entire contents are this prompt, the hidden
     <input type=file> and, on some renders, the attached-file list -- nothing
     in there wants a colour of its own. Scoping to `a` and `span` instead
     would just be a guess at which of them Garden painted.

     THE LINK KEEPS THE BOX'S COLOUR rather than taking the site link colour,
     because the whole dashed area is already the target and a blue or black
     underlined phrase inside it reads as a second, competing one. The old rule
     (css-page-new-request.css:243) bumped it to --step-1 and bolded it, which
     is the other half of the same problem. What it does take from Codex's
     fileUpload is the family: the "Add file" half of the prompt is
     body/component/strong/base there, which is Sora SemiBold where the
     trailing "or drop files here" is Inter regular -- so the family goes on the
     link alone and the rest of the prompt keeps the box's Inter. */
  .upload-dropzone *,
  [data-garden-id='forms.file_upload'] * {
    color: inherit !important;
  }

  .upload-dropzone a,
  [data-garden-id='forms.file_upload'] a {
    font-family: var(--font-family-primary), system-ui, sans-serif;
    font-size: inherit;
    font-weight: var(--font-bolder);
  }

  /* THE ATTACHED-FILE LIST, WHICH IS NOT INSIDE THE DROPZONE. It is a sibling
     of it -- `ul.upload-pool#request-attachments-pool` under
     .comment-attachments -- so every rule above stops at the dashed border and
     none of this row was being styled by us at all. Joy, 2026-10-24: "I'm not
     sure what that dark pill is on the attachment, but this needs some
     tweaking."

     Her console gave the whole row, which is worth writing down because it is
     Zendesk's markup and nothing in templates/ shows it:

       li.upload-item[data-upload-item][aria-busy]
         svg.upload-item-icon          the paperclip, stroke="currentColor"
         div.upload-item-spacer        aria-hidden, layout only
         a.upload-link[target=_blank]  the filename, links to the token URL
         p.upload-path   x2            empty
         p.upload-size                 empty
         p[data-upload-issue].notification.notification-alert
                                       empty -- THE DARK PILL
         span.upload-remove[role=button]
           svg.upload-icon             the x, stroke="currentColor"
         div.upload-progress           style="width: 100%"
         input[type=hidden][name="request[attachments][]"]

     ONE COLOUR EXPLAINS THE WHOLE DIM ROW. The paperclip, the filename and the
     x all came out the same grey, and the two icons take `currentColor` -- so
     Zendesk is colouring the <li> and everything inside follows. Setting it
     here is therefore all three fixes. The filename then takes the site link
     colour on top, because it is a real link to the attachment and Joy's
     standing note on this page is that links go back to the default: it
     carries a class, so css-reset.css's `a:not([class])` never reached it.

     THE DARK PILL IS AN EMPTY ERROR SLOT. Zendesk ships one
     `.notification-alert` per row for an upload failure and leaves it in the
     DOM empty when there is none, and nothing in this theme styles
     `.notification` -- grepped, zero hits in assets/ and templates/ -- so the
     fill is Zendesk's own stylesheet painting an element with no content. The
     two .upload-path and the .upload-size are empty the same way.

     `:empty` rather than `[aria-hidden='true']` or a flat `display: none`,
     because the slot has a job: the moment an upload fails Zendesk writes text
     into it, `:empty` stops matching, and the error appears. Hiding it on the
     aria attribute would hide it then too, and hiding it outright would mean a
     failed upload reported nothing at all.

     No !important anywhere here, unlike the dropzone above. That box is Garden,
     which emits a generated class from a runtime <style> after everything; this
     row is plain Zendesk CSS on single classes, so (0,3,1) with the page scope
     is enough. And no layout: .upload-progress is a div carrying
     `style="width: 100%"`, so putting `display: flex` on the <li> would turn it
     into a flex item and could stretch the row. Zendesk's layout already reads
     correctly -- only the colours did not. */
  .upload-item {
    color: var(--neutral-text);
    font-size: var(--step-0);
    line-height: var(--leading-body-0);
    transition: background-color var(--motion-duration-control) var(--ease-out);
  }

  /* Joy, 2026-10-24: "All looks good except when you hover over the attachment
     in dark mode" -- a near-white band appeared behind the whole row.

     It is not ours. The only background-colour hover in reach was the
     `a:not([class])` tint in css-reset.css, and that one resolves
     --neutral-bg-highlight, which is #1e2a35 in the dark tier -- darker than
     the row, not lighter -- and the filename carries a class so it never
     matched anyway. So Zendesk ships its own `.upload-item:hover` fill, and
     being a light-mode value with no dark tier it inverts the row.

     Declaring a hover of our own is enough to take it: Zendesk's is plain
     help-center CSS on a single class (0,2,0), this is (0,3,0) with the page
     scope, and it is declared later besides. No !important -- that is for
     Garden's runtime <style>, which is what the dropzone above has to fight,
     not for this.

     --neutral-bg-component-soft-hover rather than removing the hover outright.
     The row has a real pointer target in it (the x), Codex's component-soft
     hover is the quiet version of exactly this, and it is the same token the
     dropzone uses above and the requests table uses on its rows -- so the whole
     theme responds to a pointer the same way. */
  .upload-item:hover {
    background-color: var(--neutral-bg-component-soft-hover);
  }

  .upload-item .upload-item-icon {
    color: var(--neutral-icon);
  }

  /* The filename. A real link to the attachment, so it takes the default link
     treatment in full -- --hyperlink-link at rest, --brand-text on hover. Both
     have to be restated rather than inherited from `a:not([class])`, because
     the anchor carries a class. The hover was the underline's weight until
     2026-10-25; see the note on that rule in css-reset.css for why it is a
     colour now. */
  .upload-item .upload-link {
    color: var(--hyperlink-link);
    transition: color var(--motion-duration-control) var(--ease-in-out);

    &:hover,
    &:active {
      color: var(--brand-text);
    }
  }

  .upload-item .upload-remove {
    color: var(--neutral-icon);
    cursor: pointer;
    transition: color var(--motion-duration-control) var(--ease-in-out);
  }

  .upload-item .upload-remove:hover {
    color: var(--neutral-text);
  }

  .upload-item :where(.upload-path, .upload-size, [data-upload-issue]):empty {
    display: none;
  }

  /* ---- The attached-file list, Garden's version ----
     THE TWO PAGES DO NOT SHARE THIS ONE ELEMENT, which is the gap Joy's
     2026-10-25 note turned up: "we need to make sure the styles used on the
     request page WYSIWYG, file upload, etc. are used on the new_request page."

     Everything else in this section already is. The editor shell, the toolbar,
     the editable area and its <p>, the textarea flash fix and the dropzone are
     all scoped to both page classes at the top of this block, and the dropzone
     is written to both markups on purpose. The one thing that was NOT shared is
     the row you get after a file is attached, because the two pages are handed
     different components for it:

       request page       Zendesk's own markup -- ul.upload-pool >
                          li.upload-item, styled directly above.
       new request page   Garden's FileList, which is React and emits
                          data-garden-id instead. Five ids, all of them with
                          no rule anywhere in this theme before now:
                          forms.file_list, forms.file_list.item, forms.file,
                          forms.file.icon and forms.file.close / .delete.

     So the dashed box was styled on both pages and the row under it on one.
     This is the same class of defect as the "Subject" label: Garden's own
     colours are single values with no dark tier, so an attached file on the
     dark new request page comes out a pale row, exactly as those labels came
     out mid-grey.

     THE TREATMENT IS .upload-item's, DECLARATION FOR DECLARATION -- the same
     ink, the same --step-0, the same component-soft hover, the same
     --neutral-icon on the icon and the dismiss control going to full ink on
     hover. That part is not a new decision: Joy signed it off on 2026-10-24
     after reading the request page's row off her own console, and the point of
     this block is that the two pages stop diverging.

     TWO THINGS IT ADDS THAT .upload-item DOES NOT NEED. Garden draws its own
     border and fill on the row, so unlike Zendesk's <li> this one has a surface
     that has to be named or it stays single-value -- hence --neutral-bg and
     --neutral-border-soft, the quiet pair the cards already use. And every
     declaration carries !important, which the .upload-item rules deliberately
     do not: Garden is CSS-in-JS and emits a generated single class from a
     runtime <style> in <head>, after every sheet we control, so specificity
     cannot reach it. Same reason the dropzone above is !important and for the
     same component library. The `color: inherit` descendant rule is the
     dropzone's trick reused -- it hands Garden's inner <span>s and <button>s
     the row's ink without this file guessing which of them Garden painted.

     NO LAYOUT, on the same argument the .upload-item note makes: Garden's own
     flex row reads correctly and only its colours do not.

     CONFIRMED ON A LIVE PAGE, 2026-10-25. This note used to say the opposite
     and it is kept because the reason it was uncertain is still the reason to
     be careful here: the ids live in shared-bundle.js and the form bundle
     imports the component minified, so no grep of the page bundle or the
     templates can tell you whether FileList mounts at all. Joy attached a file
     and sent the render, and the row draws as these rules describe it.

     ONE THING THE RENDER SETTLED THAT IS NOT A GAP. The filename here takes
     `color: inherit` from the `*` rule below, so it is the row's neutral ink,
     where the request page's `.upload-item .upload-link` above is
     --hyperlink-link. That looked like a consistency gap and is not one:
     shared-bundle.js builds this element as `In.div.attrs({"data-garden-id":
     "forms.file"})` -- a <div>, with no href, because the file is staged
     client-side and has no attachment URL until the ticket is submitted. The
     request page's filename IS an anchor to a real attachment. Link colour on
     a div that does not navigate would be a false affordance, so the two rows
     differ here on purpose. */
  [data-garden-id='forms.file'],
  [data-garden-id='forms.file_list.item'] > [data-garden-id='forms.file'] {
    border-color: var(--neutral-border-soft) !important;
    border-radius: var(--kb-field-radius) !important;
    background-color: var(--neutral-bg) !important;
    color: var(--neutral-text) !important;
    font-size: var(--step-0) !important;
    line-height: var(--leading-body-0) !important;
    transition:
      background-color var(--motion-duration-control) var(--ease-out),
      border-color var(--motion-duration-control) var(--ease-out) !important;
  }

  [data-garden-id='forms.file']:hover {
    border-color: var(--neutral-border) !important;
    background-color: var(--neutral-bg-component-soft-hover) !important;
  }

  [data-garden-id='forms.file'] * {
    color: inherit !important;
  }

  [data-garden-id='forms.file.icon'] {
    color: var(--neutral-icon) !important;
  }

  /* Garden ships two dismiss affordances and which one appears depends on the
     file's state -- `close` while an upload is in flight, `delete` once it has
     finished -- so both get the control treatment rather than guessing which
     this form uses. The pointer cursor is restated for the same reason it is on
     .upload-remove above: these are <button>s in Garden's markup on some
     versions and spans with role=button on others. */
  [data-garden-id='forms.file.close'],
  [data-garden-id='forms.file.delete'] {
    color: var(--neutral-icon) !important;
    cursor: pointer;
    transition: color var(--motion-duration-control) var(--ease-in-out) !important;

    &:hover {
      color: var(--neutral-text) !important;
    }
  }
}
/* !SECTION - Brand refresh: shared form controls */
