/* Hero */

/* NOTHING HERE. The hero band is the shared `.kb-page-hero` component as of
   2026-10-26 (css-components.css, inside the html.category-page scope), so this
   page states nothing about it -- which is the point of the conversion.

   `.hero-section-name { line-height: 1.3 }` lived here for a few hours on
   2026-10-26, propping up the <h1>-to-<p> demotion of the section name: a <p>
   inherits line-height 1.5 from css-reset.css where a heading gets 1.3, and at
   --step-6 that was about 6px of extra band height. `.kb-page-hero-title` now
   sets --leading-heading-7 (1.15) directly and at (0,2,1) outranks any such
   rule, so it was deleted rather than left to lose silently. */

/************************/

/* Sidebar */

/* Sticky Nav*/

.sticky {
  display: none;
}

@media (min-width: 60em) {
  /* .support-info is NOT .sticky's layout and must not be given it. It belongs
     to new_request_page, and these two declarations are the only reason it is
     here at all -- they were written when both columns wanted the same thing.

     LEAVE THIS EXACTLY AS IT IS, and know that it is wrong. css-page-new-request
     loads at document_head.hbs:226, this file at :227, and both target
     `.support-info` at (0,1,0) -- so inside this media query the rule here wins
     on source order and overrides that component's own
     `display: flex; flex-direction: column; gap: 1rem`
     (css-page-new-request.css:90) with `display: block`, on every viewport wide
     enough to show a sidebar at all. Its flex-direction and gap have therefore
     never applied, and the spacing inside that card comes from its children's
     margins instead.

     Deleting the selector restores the component and changes the spacing on a
     page that is already signed off, so it is Joy's call and not a side effect
     of the nav work. Raised 2026-10-26. The `gap: 2rem` is inert either way --
     gap does nothing on a block container. */
  .support-info {
    display: block;
    gap: 2rem;
  }
  /* NO max-width. This carried `max-width: 16.5rem` until 2026-10-26, which
     silently won over the width this panel is supposed to have: it is the first
     child of `.sidebar`, so css-utilities.css gives it
     `flex-basis: var(--sidebar-target-width, 21.25rem)`, and a max-width clamps
     the flex item's used size whatever the basis says. The panel was therefore
     264px while the two things it is meant to match were 340px -- the Support
     Info panel on new_request_page, which is the other `.sidebar` consumer and
     sets no max-width of its own, and the search results facet column, which is
     a flat 21.25rem grid track. That 21.25rem is the number Joy signed off on
     2026-10-23 off Figma's 339px.

     Joy, 2026-10-26: "as long as everything is consistent with the other pages,
     I'm happy." So the pin goes rather than being retuned, and the shared
     default is the single place the width is stated.

     SUPERSEDED IN PART ON 2026-10-27: this column is now 16rem/256px while
     those two stay at the shared 21.25rem, by Joy's decision. The paragraph
     above still stands as the reason there is no `max-width` here -- a cap
     would clamp the flex item's used size and the column would stop tracking
     whatever the token says. The narrowing is done by overriding the token on
     `.sticky` (see the rule inside the 60em query below), which is the
     supported way to change this and leaves the no-max-width rule intact.

     Knock-on worth knowing: the content column beside it is
     `flex-grow: 999`, so it absorbs the difference and narrows by the same 76px.
     That is the same split new_request_page already has. */
  .sticky {
    /* THIS PAGE'S SIDEBAR IS NARROWER THAN THE OTHER TWO, ON PURPOSE.
       2026-10-27. The shared default is still 21.25rem/340px and
       new_request_page's Support Info panel and the search results facet track
       both still take it -- only this column opts down to 16rem/256px, which is
       the width Codex's own docs aside ships at (`w-[256px] shrink-0`, measured
       in DevTools rather than read off a frame, and pinned so it does not flex
       at any viewport).

       Joy's call, and the reason it does not break the consistency she asked
       for on 2026-10-26: those pages are never seen beside each other, so the
       difference in proportion is not noticeable. Her words --- "it looks
       different enough to where the proportion sizes won't be noticeable."

       DECLARED ON `.sticky` RATHER THAN ON A PAGE WRAPPER because this is the
       element the `flex-basis: var(--sidebar-target-width, 21.25rem)` in
       css-utilities.css actually applies to (it is `.sidebar > :first-child`),
       and a custom property is resolved on the element the declaration using
       it matches. `.sticky` is on one element in the whole theme,
       article_page.hbs:99, so this cannot reach another page even by accident.
       The var() fallback in css-utilities.css stays the shared number; this is
       the per-page pin that comment anticipated.

       It lives inside the 60em query because that is where this column has a
       width at all -- `.sticky` is `display: none` below it (:20), so a
       narrower basis there would describe a box that is not laid out.

       WHAT THIS DOES *NOT* NEED, and why: `--sidebar-content-min-width` is
       untouched. The pair wraps when the basis plus the content column's 50%
       minimum out-runs the container, which this moves from roughly 680px of
       container to 512px -- but this page is one column below 60em regardless,
       so the new wrap point sits inside a range where there is no sidebar to
       wrap. On new_request_page, which has no media query and would have felt
       that shift as a two-column form at tablet width, the default is
       unchanged, so nothing moves. */
    --sidebar-target-width: 16rem;
    /* ONE OFFSET, STATED ONCE. The panel pins 4rem below the top of the
       viewport, so the tallest it can be and still *end* on screen is the
       viewport less that same 4rem. Until 2026-10-26 these were two
       independent numbers -- `inset-block-start: 4rem` against
       `max-height: 100vh` -- and the mismatch put the last 4rem of the panel
       permanently below the fold. Anything scrolled to the bottom of it was
       unreachable: the box is pinned, so its own scrollbar moves content
       *within* a box whose foot is off screen, and no amount of scrolling
       brings that strip up. Both values now read from the one property, which
       is the only reason they cannot drift apart again. */
    --sticky-offset: 4rem;
    /* ROOM FOR A HOVER FILL TO GROW INTO, and the only reason this column has
       inline padding at all (2026-10-27). The two section rows want padding
       inside their hover fill while their labels stay level with the eyebrow
       and the rail, which means the fill has to grow outward past the rail --
       and `overflow-x: hidden` below clips at this element's PADDING box, so
       without padding here there is nothing to grow into and the fill is simply
       cut off. That is what defeated an earlier attempt.

       STATED HERE, ONCE, because the row's `padding-inline` and its negative
       `margin-inline` both read it (:924-ish) and must cancel exactly. Two
       literals that happen to match would shift the content the first time one
       of them was retuned.

       0.5rem is the inset .kb-footer-link uses, which is the rail this hover
       was lifted from in the first place, so the two still agree.

       WHAT IT COSTS: the column's content box is 1rem narrower than its
       16rem/256px border box (box-sizing is border-box sitewide,
       css-reset.css:5, so the flex-basis still buys 256px of column -- this
       does not widen anything). The whole rail therefore sits 0.5rem in from
       the column edge. That is closer to the reference, not further from it:
       Codex's own aside is 256px with 24px of inline padding, i.e. 208px of
       content, and this leaves appreciably more than that. */
    --rail-bleed: 0.5rem;
    padding-inline: var(--rail-bleed);

    /* WHERE THE RAIL SITS, and it is a derived number rather than a chosen one.
       Joy, 2026-10-27: "indent the dropdown list even more so that the left bar
       is positioned to the center right underneath the icon, currently it's
       positioned almost to the outside left." Measured off her Codex
       side-navigation reference, the group's rule descends from the middle of
       the group header's own icon, not from the container edge -- which is what
       makes a list read as hanging off its header rather than sitting beside it.

       SO IT IS HALF AN ICON, LESS HALF A RAIL. The section glyphs are pinned to
       a 1em box at `font-size: var(--step-0)` (:1039-ish), so each icon occupies
       exactly --step-0 of inline space whatever glyph is in it and its centre is
       --step-0/2 from the column's content edge. Backing off half the rail's own
       width centres the rule on that point instead of starting it there.

       THE 1em PIN IS A PREREQUISITE, NOT A COINCIDENCE. Before it, the three
       glyphs were three different widths -- 448, 512 and 640-unit viewBoxes --
       so "the centre of the icon" was three different numbers and this could
       only ever have been eyeballed against one of them. Pin the box, and one
       declaration is correct for all three sections.

       STATED ONCE HERE, beside --rail-bleed and for the same reason: three lists
       consume it (:572-ish and :1202-ish) and a rail that disagreed between the
       groups is the one thing that would stop the column reading as continuous.
       Do not inline the calc at the consumers. */
    --rail-indent: calc((var(--step-0) - var(--border-width-heavy)) / 2);
    position: sticky;
    inset-block-start: var(--sticky-offset);
    max-height: calc(100vh - var(--sticky-offset));
    z-index: 1000;
    /* A flex column so the heading list can be told to take whatever height is
       left over after the two cards below it -- see the group under this rule.
       `display` is declared here rather than in the shared rule above because
       that rule also matches .support-info, which must keep `display: block`
       for the reason given there.

       This also wakes nothing up: the `gap: 2rem` that sits in the shared rule
       was inert on a block container and is NOT inherited into this one, so the
       1rem that separates the cards is still their own margin-block-start
       (:785) and the spacing is unchanged. */
    display: flex;
    flex-direction: column;
    /* `auto`, not `scroll`. Until 2026-10-26 both axes were `scroll` (and
       `overflow-x` was stated twice), which draws the tracks on every article
       whether or not there is anything to scroll -- a short nav got a dead
       vertical bar and every nav got a dead horizontal one. `auto` asks for a
       track only when the list really is taller than the panel, which is the
       call the hamburger's locale list already makes (css-page-header.css:1188).

       The horizontal axis is `hidden` rather than `auto` because nothing is
       meant to reach it: the anchors carry `overflow-wrap: break-word` (:600)
       precisely so a long unbroken product name breaks instead of pushing this
       column sideways. A bar on an axis whose overflow has been designed out
       could only ever be the symptom of a bug, and `hidden` leaves such a bug
       visible as clipping rather than hiding it behind a scrollbar nobody
       expects to use.

       `scroll-snap-stop: always` sat here too and was inert: it does something
       only on a scroller whose children carry snap positions, and
       `scroll-snap-type` is set nowhere in this theme. Removed rather than
       completed -- a nav that snaps as it is dragged is not wanted.

       scrollbar-gutter stays. With `scroll` the bar always reserved its space,
       so keeping the gutter reserved is what holds this panel's content at the
       width the rail indent was measured against. */
    overflow-y: auto;
    overflow-x: hidden;
    /* Was `var(--clr-neutral-40) var(--clr-white)` -- legacy Copenhagen
       primitives, one value each and no dark tier, so dark mode drew a white
       track behind a mid-grey thumb. The theme already has a token pair for
       exactly this with both tiers, and two other scrollers use it unchanged
       (css-buttons.css:485, css-page-header.css:1189). `transparent` lets the
       panel's own background show through instead of naming a surface here.
       Width and thumb rounding are left to the UA for the reason recorded at
       css-page-header.css:1155 -- setting either standard property makes Chrome
       drop ::-webkit-scrollbar wholesale, so there is no second implementation
       to disagree with this one. */
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
    scrollbar-gutter: stable;
    /* Keeps a keyboard-focused row off this scroller's own clipping edge. An
       outline is clipped by an overflowing ancestor, so without it the focus
       ring is shaved on whichever row scrolling happens to land flush against
       the top or bottom -- the same reason the hamburger's locale list sets it
       (css-page-header.css:1187). The horizontal half of that problem is
       handled differently, by the rail taking the ring's colour (:643), because
       no padding exists on this axis to scroll into. */
    scroll-padding-block: 0.25rem;
    /* Breathing room under the last card. With both cards open the column is
       out of room and everything in it is flush against the bottom edge, which
       reads as cut off rather than as the end of the panel. `box-sizing` is
       border-box theme-wide (css-reset.css:5), so this comes out of the cap
       rather than being added to it -- the column cannot outgrow its container
       and start being shoved up again (see article-nav-seek.js). It is also
       inside the scrollport, so it shows as trailing space once the column's
       own scrollbar is in play, which is exactly when it is wanted. */
    padding-block-end: 1rem;
  }

  /* THE SCROLL BELONGS TO THE LIST, NOT THE COLUMN. Joy's call, 2026-10-26,
     from Codex's side navigation: the heading label holds still while the rows
     scroll under it, so a long table of contents -- which on this knowledge
     base is the common case rather than the exception -- can no longer push
     Recently viewed and Related articles out of reach below the fold.

     NO `position: sticky` ON THE LABEL, deliberately, even though that is how
     this effect is usually built. The scroller is the inner <div>, which leaves
     the label a sibling *outside* it, holding still by construction. Sticky
     would mean moving the scroller up to the <nav>, which puts the label inside
     it and then needs an opaque background to stop rows showing through as they
     pass -- and the only honest value for that background is the page's own,
     which is --page-bg-middle *plus* a --page-bg-corner radial glow
     (css-reset.css:209). A flat colour laid under a gradient is a visible
     block. Nothing is given up by avoiding it: a part-scrolled list still shows
     a clipped row at its top edge, which is the same "there is more above" cue
     the reference gets from the overlap.

     WHY THE SELECTOR CARRIES [data-spy]. Both the <div> and the <ol> injected
     inside it have class `table-of-contents` (see the note below), so the bare
     class would make a scroller of each and nest one inside the other. The
     attribute is on the div alone, which is also how article-nav-seek.js picks
     it out.

     HOW THE HEIGHT IS DIVIDED. .sticky is capped at the viewport less its own
     offset. Every item in the column takes its natural height; when they add up
     to more than the cap, the nav -- and the list inside it -- is the only thing
     allowed to give any back.

     The grow factor is 0 on purpose, and getting that wrong is visible. With
     `flex: 1` the list claims the whole remainder whether it needs it or not, so
     on a short table of contents the leftover height lands *inside* the list box
     and both cards get pushed to the floor of the viewport with a dead band
     above them. Shrink was the half of `flex` this ever wanted.

     `min-height` is what makes the shrinking possible -- a flex item will not go
     below its content size without it, and that omission is the usual reason a
     nested scroller silently refuses to scroll.

     WHICH ELEMENT CARRIES THE FLOOR MATTERS, and getting it wrong is also
     visible. The floor belongs on the nav. Put it on the list instead and give
     the nav `min-height: 0`, and with both cards open the nav shrinks smaller
     than the list it contains -- but the nav has visible overflow, so the rows
     spill out of it and paint *over* the cards rather than being clipped. The
     list is the one of the two that can clip, so the list is the one allowed to
     go to zero. Once the floor itself will not fit, .sticky's own scrollbar
     takes over, which is what it is still there for. */
  .article-toc {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    /* THE COLLAPSED FLOOR, and it is here so the open floor below has something
       to interpolate from. Same value and same meaning as
       `.sidebar-article-dropdown > summary { min-height }` (:920): the nav must
       never be shorter than the one row it always shows. Stated as a length
       rather than left at the flex item's `auto`, which protects the same row to
       the same height but cannot be animated to or from. See the note on the
       open floor for what that buys. */
    min-height: 2.25rem;
    transition: min-height 400ms ease-out;
  }

  /* Heading plus a few rows. Low enough to stay out of the way until the column
     is genuinely out of room.

     CONDITIONAL ON `open` SINCE 2026-10-27, and that is not a refinement -- it
     is what stops the floor becoming a hole. The nav's content when collapsed is
     one 36px summary row, so an unconditional 8rem floor would hold 128px of
     empty column open under a closed header and push the two cards down into it.
     The floor only ever meant "do not squash the list to nothing"; with no list
     rendered there is nothing to protect.

     IT IS TRANSITIONED FROM THE COLLAPSED FLOOR, not from nothing, and that is
     the whole reason the collapsed floor is declared as a length up there.

     `:has()` starts matching in the same frame as the click, so left alone this
     floor arrives in one step: measured 2026-10-27, .article-toc went 38 -> 128
     in a single frame, which shoves the two cards down 90px before the list has
     rendered a row. On a long article that is nearly invisible, because the list
     is past 128px on its own content within 47ms and the eye reads one
     continuous sweep. On a SHORT article it is the whole animation -- the list
     never grows into the floor, so the cards jump and then nothing moves. Joy
     reported exactly that split, long smooth and short hitching, which is what
     identified the floor as the remaining cause.

     An earlier attempt put `transition: min-height` on .article-toc while
     leaving its base at the flex item's `auto`. That is inert: `auto` is not an
     interpolable length, so the floor snapped anyway. The fix was not to drop
     the transition but to give it a length to start from. `min-height: 0` would
     also be interpolable and is the wrong choice for the reason set out in the
     paragraph on .article-toc above -- with no floor at all, column pressure
     squashes the nav below its own summary row and the heading paints over the
     cards. The summary's own floor is both interpolable and exactly the
     protection that paragraph asks for.

     The nav's used height is max(content, floor), so this composes rather than
     competes: on a long article the content curve wins almost at once and the
     floor is never seen; on a short one the floor curve carries the last part of
     the sweep. Both run 400ms ease-out, the same as the list, so the crossover
     is a change of slope and not a step. Closing runs it in reverse for free.

     The duration is the fifth 400ms in this component and belongs with the
     others in one custom property -- still a separate change. */
  .article-toc:has(> .sidebar-article-dropdown[open]) {
    min-height: 8rem;
  }

  /* THE SHRINK NOW HAS TWO MORE BOXES TO CROSS (2026-10-27). Before option 2 the
     chain was nav -> div and the two rules above/below were the whole story.
     With the table of contents inside a <details> it is
     nav -> details -> ::details-content -> div, and a flex chain is only as
     shrinkable as its least shrinkable link: ONE box left at `min-height: auto`
     anywhere along it pins the whole column open and .sticky's own scrollbar
     takes over for no reason. Both of these are that fix and neither is
     cosmetic.

     `flex: 0 1 auto` on the details is also an OVERRIDE of `.sidebar-article-
     dropdown { flex: none }` below, which is right for the two cards -- they
     have fixed-length lists and must not be squashed -- and wrong for this one,
     which is the only panel in the column built to scroll. Specificity does the
     work: 0,2,0 against 0,1,0.

     ::details-content is the UA's wrapper around everything but the summary, so
     it is a real flex item with real automatic minimum size even though no
     markup declares it. It is easy to forget for exactly that reason. */
  .article-toc .sidebar-article-dropdown {
    flex: 0 1 auto;
    min-height: 0;
  }

  .article-toc .sidebar-article-dropdown::details-content {
    flex: 0 1 auto;
    min-height: 0;
  }

  /* The summary is the nav's fixed part, and shrink is inherited from the
     default. Under pressure it would hand over its own line box along with the
     list, squashing the heading instead of scrolling the list. This is what
     `.article-toc-label { flex: none }` did before the conversion -- the job
     moved with the heading rather than going away. */
  .article-toc .sidebar-article-dropdown > summary {
    flex: none;
  }

  .table-of-contents[data-spy] {
    flex: 0 1 auto;
    /* The licence to shrink past the content. No floor here on purpose -- it
       lives on the nav above, for the reason set out there. */
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    /* Same pair and same reasoning as .sticky above. Deliberately NO
       scrollbar-gutter: .sticky already reserves one, and a second would narrow
       the rows by another thin bar against the indent measured off the
       reference. */
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
    scroll-padding-block: 0.25rem;
  }

  /* The cards keep their height. Left at the default `flex: 0 1 auto` they
     would surrender it to the list under pressure, squashing a card that has
     `overflow: hidden` on it instead of scrolling the one thing here that is
     built to scroll.

     THE TABLE OF CONTENTS IS NOW ALSO ONE OF THESE and is the exception,
     overridden above -- it is the one thing here that is built to scroll. Do not
     reach for `:not(.article-toc *)` to express that; the override is already
     written and reads in the direction the reasoning runs. */
  .sidebar-article-dropdown {
    flex: none;
  }
}

/************************/

/* Table of contents (inner sticky nav) -- the Codex "on this page" rail, inside
   a collapsible section like the other two.

   ON THIS BRANCH IT IS A .sidebar-article-dropdown AGAIN, and the sentence here
   used to say the opposite. Joy's 2026-10-26 call -- choosing between flattening
   all three sidebar panels, retokenising all three as cards, and flattening this
   one only -- took the third option, because the table of contents is the
   primary sidebar nav and is always wanted open, where Recently viewed and
   Related articles are secondary and stay collapsible so the column cannot run
   away in length. Our articles carry far more h2s than a Codex docs page, which
   is why the collapsible pair earns its keep.

   Option 2 (2026-10-27) keeps every word of that and reads the conclusion
   differently: "always wanted open" is a DEFAULT, which the `open` attribute
   states exactly, and an article with thirty h2s is the strongest argument for
   letting a reader shut the list rather than the weakest. So all three sections
   are now the same component -- icon, eyebrow, chevron, railed list -- which is
   what Joy asked for: "it would look like one continuous thing."

   WHAT IS STILL SPECIAL ABOUT THIS ONE is not its chrome, it is that it SCROLLS.
   It is the only panel whose list has no length it is known to stay under, and
   that fact drives the three things that are written differently for it: the
   flex chain in the 60em query above, the `0fr <-> 1fr` collapse below, and the
   conditional 8rem floor. The visual rules in this section are shared with the
   two cards and need no exceptions.

   THE REFERENCE IS A SCREENSHOT, NOT A FRAME. There is no Codex design for a
   sticky nav; Joy supplied their side-navigation documentation page, whose
   right-hand column carries their own "on this page" nav. Everything below is a
   ratio taken off that image rather than a pixel lift, because it is resampled
   at 1.75x and so has no colour plateau to read exact values from. Measured on
   the image, in device pixels: rows on a dead-even 53.6px pitch, a continuous
   3px rail, the eyebrow's ink starting at x75 flush with the rail at x72-74,
   and the item text indented to x94. Divided through, that is a ~31px row, a
   2px rail and a ~12.6px item indent past the rail. The 1.75x figure is from
   two cap-height measurements that both gave 0.70 of the em, which is Sora's
   cap height exactly; a 2px CSS border does render as 3 device px at that DPR.
   The colours come from the Codex variable table, not from the image.

   TWO ELEMENTS CARRY THE CLASS `table-of-contents`: the <div> in the template
   and the <ol> that table-of-contents.js injects inside it. The extension
   renders with `replaceContent: true`, so the div keeps its own class and gains
   a child with the same one. The nested `ol` below is the injected list, and the
   outer rules apply to the div.

   WHY THE ACTIVE ROW IS INK AND WEIGHT, NOT A FILLED PILL AND NOT A RAIL
   SEGMENT. Codex does use a `neutral/bg-component` pill for the active row of
   its left-hand site navigation, and that is the element the Figma frame
   actually documents -- but it is a different component. Their right-hand "on
   this page" nav, which is what this is, marks the current heading with weight
   and stronger ink only.

   THE RAIL IS UNIFORM AND STAYS THAT WAY. Sampled at eight row centres on the
   reference, including the active row, it is (204,215,217) every single time --
   --neutral-border-soft end to end, with no active segment anywhere. Codex
   separates the active row by ink and weight alone: (17,23,29) against
   (82,95,98) for the rest.

   A darker active segment shipped here briefly on 2026-10-26, because the
   preview Joy chose described the active row as "semibold + rail segment" and
   I had inferred the segment from the image rather than measuring it. The
   measurement above disproved it and Joy removed it the same day. Do not add
   `border-inline-start-color` back to .is-active -- the rail is a continuous
   rule, and breaking it into a differently coloured span is the one thing the
   reference specifically does not do.

   THE ONE EXCEPTION, and it is not this one: `a:focus-visible` does recolour
   the rail, to the focus ring's own colour. That is a transient keyboard state
   rather than a persistent marker of where you are, and it is the only cue on
   these rows that `overflow-x: hidden` cannot clip. See the note on that rule.

   `is-active` lands on the <a>, not the <li>: extensions.js:1442 does
   `activeLink.classList.add`, and scrollspy collects `a[href^='#']`. So every
   active rule here hangs off the anchor and none of them need :has(). */

.article-toc {
  /* The whole nav disappears when the article has no h2. tmpl-table-of-contents
     is wrapped in `if items.length >= 1`, so in that case the extension leaves
     the div empty and the eyebrow label would sit over nothing. Keyed on the
     injected <ol> because that is the only thing whose presence proves the
     extension found headings.

     KNOWN TRADE-OFF: table-of-contents.js runs on `window load`, so the <ol>
     does not exist until then and this hides the nav until it does. The rows
     always appeared that late; what is new is that the label now arrives with
     them instead of sitting there empty. The alternative is an orphan eyebrow
     on every article with no h2, which is worse. If the pop-in reads badly,
     the fix is to render the label from the template only when the body has a
     heading, not to drop this rule. */
  &:not(:has(ol)) {
    display: none;
  }
}

/* TOMBSTONE: `.article-toc-label` (deleted 2026-10-27, option 2). It dressed the
   "In this article" heading as the eyebrow -- --step--1, --font-bold, 1.2,
   uppercase, 0.06em, --neutral-text-soft, --space-2xs below -- and all of that
   now comes from `.sidebar-article-dropdown .sidebar-articles-label h3` plus the
   summary's own `color`, which is where the other two sections have always got
   it. The class came off the h3 in the template; the id stayed, because
   aria-labelledby points at it.

   TWO OF THOSE DECLARATIONS WOULD HAVE BEEN BUGS if the class had been left on,
   which is why this is a deletion and not a harmless duplicate: `color` would
   have re-broken the summary's one-colour-per-state inheritance (the h3 pinning
   its own ink while its sibling glyph and the chevron inherit the summary's is
   the exact defect fixed on the 27th -- see the note at the h3 rule), and
   `margin-block-end` would have opened --space-2xs of dead space inside the
   summary row, between the label and nothing.

   The reasoning worth keeping, because it is about the LAYOUT and still true:
   the heading starts flush with the column's content edge and the rows sit
   indented inside it, so the label reads as the thing the list hangs under. A
   padding that lined the heading up with the item text instead shipped briefly
   on 2026-10-26 and made the list look like it had escaped its own heading;
   measuring the reference showed it does the opposite. The indent is the
   anchors' own padding-inline-start, and there is still nothing to keep in step
   with it here. */

.table-of-contents {
  width: 100%;

  ol {
    display: flex;
    flex-direction: column;
    /* AND EXPLICITLY NOT `width: 100%`, which is what this list had been getting
       and what cost the focus ring its inline-end edge. Joy found it on
       2026-10-29: "the right border is missing on article headings."

       TWO ELEMENTS CARRY THE CLASS `table-of-contents` -- the <div> in the
       template and the <ol> table-of-contents.js injects inside it, which the
       note above this rule already spells out. So the `width: 100%` written for
       the div was landing here as well, and against `margin-inline-start:
       var(--rail-indent)` below that makes this list exactly one rail indent
       WIDER than the box that clips it. Every row overhung the scroller's
       padding edge by that much and `overflow-x: hidden` took the difference.

       `auto` is not a tweak, it is what a block-level box in flow does on its
       own: margin plus width plus margin resolves to the containing block, so
       the indent comes out of the width instead of being added to it. The
       panels' <ul> never had this bug for exactly that reason -- it carries the
       same `margin-inline-start` and no width at all -- which is why Joy saw the
       edge missing on the heading rows only.

       NOTHING WAS VISIBLY WRONG BEFORE THE RING, which is why it stood. The
       overhang is ~7px of row that holds no text (the anchors' `padding-inline`
       keeps the labels well clear of it) and the rows carry no fill or border on
       that edge at rest, so there was nothing out there to clip. A ring is the
       first thing this component has ever drawn on its inline-end edge. */
    width: auto;
    /* THE GAP UNDER THE HEADER, and it lives here because the heading's
       `margin-block-end` no longer exists to supply it. That was
       `.article-toc-label { margin-block-end: var(--space-2xs) }`, deleted with
       the class on 2026-10-27 when the heading moved into a <summary> -- and the
       space went with it, which Joy caught in preview: "there needs to be more
       space between In this article and the dropdown. Recently viewed is
       correct."

       SAME DECLARATION AS THE TWO PANELS' `ul` (:1222) rather than a value of its
       own, which is the point -- "Recently viewed is correct" means the panels
       are the reference now, and the three groups each state the gap the same
       way. The trailing 0 is theirs too.

       IT SCROLLS AWAY WITH THE LIST, unlike the margin it replaces, because this
       <ol> is inside the scroller and that margin was outside it. That is the
       right behaviour and not a compromise: at rest there is a gap, and a list
       scrolled down runs to the top of its own box like any other scrolling
       list. A `margin-block-start` on the scroller would hold the gap open
       permanently -- but it would also overflow the collapsed `0fr` row and show
       as a strip of dead space under a closed header, which is the exact
       uniformity Joy asked for in the same message. */
    padding-block: var(--space-2xs) 0;
    /* The rail lands under the middle of the section icon. One number, declared
       on .sticky, consumed identically by all three lists -- see the long note
       at --rail-indent for where it comes from and why it is not a judgement
       call. MARGIN, NOT PADDING: the rail is a border on each row, so the rows
       have to move with it, and only a margin on the list moves both. */
    margin-inline-start: var(--rail-indent);
    /* NO gap, deliberately. The rail is a border on each anchor, so the rows
       have to be contiguous or the rule breaks into dashes. Row rhythm comes
       from the anchors' own padding instead. */
    gap: 0;
  }

  a {
    display: flex;
    /* THE ROW IS THE FOCUS RING'S CONTAINING BLOCK, and that is all this is for
       -- nothing else in this list is positioned. See the `:focus-visible` note
       below for why the ring is a pseudo-element rather than an outline. */
    position: relative;
    /* The rail. Drawn on every row at --neutral-border-soft and left alone by
       the active row, so it reads as one continuous rule down the list and
       nothing shifts by a pixel as the active item changes under the
       scrollspy. The rows are contiguous (no gap on the ol) precisely so these
       per-anchor borders join up into that single rule. */
    border-inline-start: var(--border-width-heavy) solid
      var(--neutral-border-soft);
    padding-block: var(--space-2xs);
    padding-inline: var(--space-2xs) var(--space-3xs);
    font-size: var(--step-0);
    font-weight: var(--font-regular);
    line-height: 1.3;
    color: var(--neutral-text-soft);
    text-decoration: none;
    /* Heading text is author-written and can carry long unbroken product
       names; the column is 256px as of 2026-10-27 (it was 340px when this was
       written), so let them break rather than push the sticky column into its
       own horizontal scroll. There is 84px less to break into now, which is
       why this matters more than it did, not less. */
    overflow-wrap: break-word;

    &:hover {
      background-color: var(--neutral-bg-component-soft-hover);
      color: var(--neutral-text);
      transition-duration: var(--transition-duration);
    }

    /* THE FOCUS RING IS A PSEUDO-ELEMENT RING, not an outline and no longer the
       rail. Joy chose the shape on 2026-10-29 from Codex's side navigation: a
       --border-width-heavy --form-focus-ring rule at --radius-sm around the row,
       with the rail left grey. Same ring as the three summary rows above, and
       the long version of the reasoning is at their `:focus-visible`.

       WHY NOT THE SITEWIDE OUTLINE. Nothing upstream of these rows declares
       inline padding -- `.sidebar` sets flex-basis and gap only in
       css-utilities.css, the list sets none, and css-reset zeroes the <ol> -- so
       each anchor's border box starts flush with the padding box of the element
       that clips it, and an offset ring has nowhere to paint on that axis. THE
       LIST, NOT `.sticky`, IS THE ELEMENT THAT MATTERS HERE: `.sticky` gained
       `padding-inline: var(--rail-bleed)` on 2026-10-27 so the summary rows'
       hover fill had room to bleed into, and that does nothing for these rows,
       because `.table-of-contents` is a nearer clipper with no padding of its
       own. Do not read that padding as having fixed this. Two elements clip the
       axis -- the list is its own scroller with `overflow-x: hidden` and .sticky
       is one outside it -- and naming neither in particular is deliberate: a
       mark drawn inside the row is unaffected by whichever is nearer.

       `outline: none` IS NOT REDUNDANT, because the outline is not wholly
       clipped. Its left and right edges are cut off, but its top and bottom
       survive over the middle of the scrollport -- the pair of stray blue rules
       bracketing the focused row in Joy's screenshot, which read as borders
       appearing out of nowhere. That pair is half of what this block fixes.

       WHY NOT AN INSET box-shadow, which is what the summary rows use. An inset
       shadow follows `border-radius`, and a radius on THIS row would scallop the
       rail: the rail is the row's own `border-inline-start` and the rows are
       contiguous, so rounding the corners breaks one continuous rule into a
       dashed line, once per row. An absolutely positioned ring carries its own
       radius and leaves the row's corners square.

       THE INLINE-START INSET IS ONE RAIL WIDTH. At `inset: 0` the ring would sit
       flush against the rail and the two --border-width-heavy marks would read
       as a single thick bar; holding them apart keeps it a rail plus a ring,
       which is how the reference reads.

       TOMBSTONE: `&:focus-visible { border-inline-start-color:
       var(--form-focus-ring) }`, 2026-10-26 to 2026-10-29. Joy asked for the
       rail to take the ring's colour because it was the only cue on these rows
       an `overflow-x: hidden` could not clip, and that held until this ring
       existed. It is deleted rather than kept alongside: a blue rail inside a
       blue ring is the doubled line css-reset's focus note warns about, and the
       reference leaves the rail grey. The measurement it was careful not to
       contradict still stands and now covers focus as well -- the rail is
       uniform --neutral-border-soft end to end, and no state breaks it into
       differently coloured spans. */
    &:focus-visible {
      outline: none;

      &::after {
        content: '';
        position: absolute;
        inset: 0 0 0 var(--border-width-heavy);
        border: var(--border-width-heavy) solid var(--form-focus-ring);
        border-radius: var(--radius-sm);
        /* Decoration over a row that is entirely a hit target. */
        pointer-events: none;
      }
    }
  }

  .is-active {
    /* Weight and ink only -- no rail recolour, no fill. See the note above for
       the measurement that settled this. The ink step from --neutral-text-soft
       to --neutral-text is deliberate and matches the reference; weight alone
       is a weak cue on a long list. */
    font-weight: var(--font-bold);
    color: var(--neutral-text);

    /* Hover must not walk the active row back to the soft ink. */
    &:hover {
      color: var(--neutral-text);
    }
  }

  /* CLICK-TO-JUMP: SHOW THE DESTINATION, NOT THE ROWS ON THE WAY TO IT.
     Joy, 2026-10-26: clicking a heading several rows down lit every row in
     between as the smooth scroll passed it. article-nav-seek.js marks the row
     the user actually clicked and sets `is-seeking` on the nav for as long as
     the scroll lasts; this is the half that makes the mark visible.

     The scrollspy is untouched and goes on moving `is-active` through the
     intermediate rows underneath -- these two rules just stop that being what
     you see. Specificity is why it works without touching the spy:

       .table-of-contents.is-seeking .is-active:not(.is-seek-target)  (0,4,0)
       .table-of-contents.is-seeking .is-seek-target                  (0,3,0)
       .table-of-contents .is-active                                  (0,2,0)
       .table-of-contents a                                           (0,1,1)

     The suppressing rule has to out-rank `.is-active:hover` (0,3,0) as well,
     which it does on the extra :not(). The pinning rule only *ties* with it,
     so this block has to stay after the .is-active block -- source order is
     what settles that tie. Do not move it above.

     Both rules restate the base values rather than using `revert-layer` or
     `all: initial`: there are only two declarations to undo and naming them
     keeps this readable against the `a` rule twenty lines up. */
  &.is-seeking {
    .is-active:not(.is-seek-target) {
      font-weight: var(--font-regular);
      color: var(--neutral-text-soft);
    }

    .is-seek-target {
      font-weight: var(--font-bold);
      color: var(--neutral-text);
    }
  }
}

/***************************/

/* Related and Recent Articles */

/* THERE ARE NO BARE `details` OR `summary` RULES HERE ANY MORE (2026-10-26).
   This file used to open with four of them, and they were the only thing styling
   three unrelated components: the three sidebar panels below, the share menu at
   :1437, and the author-written accordions at :2064. All five <details> in the
   theme are on this page, so a bare selector here was effectively a shared
   base class that nothing named.

   That had to go before the sidebar could be rebuilt, because the hybrid Codex
   nav drops the article-nav panel's <details> entirely -- which would have
   silently changed the share menu and the accordions along with it.

   What each consumer was actually taking, and where it comes from now:

     `display: flex` / `overflow: hidden`  -> declared on .sidebar-article-dropdown
         and on .article-share directly. The accordions never used these two from
         here; `.article-content details` (:2064) outranks a bare selector and
         declares both itself.

     `flex-direction: column`  -> was NEVER declared in this file. It reached all
         five from a second, duplicate bare `details` rule in css-components.css,
         which loads first (document_head.hbs:220 vs :227) and so lost `display`
         and `overflow` to the rule that used to be here, but kept this one
         because the rule here never mentioned it. That is why the share menu's
         dropdown stacks under its trigger instead of sitting beside it, and it
         was load-order luck rather than intent. Both panels and share set it
         themselves now, and that duplicate rule is deleted -- see the tombstone
         at css-components.css:591.

     `margin-block-start: 1rem` + `details:first-child { 0 }`  -> moved onto
         .sidebar-article-dropdown, which is the only consumer that wanted a gap.
         The share menu already zeroed it on itself (:1441) and the accordions
         zeroed it twice (`.article-accordion { margin-top: 0 }`, once in this
         file and once in css-components.css). The duplicate in this file went
         with the rest of the duplicated accordion block on 2026-10-28; the
         surviving one is in css-components.css and is redundant rather than
         load-bearing, kept only because it is harmless.

     `summary { display: flex; align-items: center; justify-content:
         space-between }`  -> redundant for the panels and the share menu, which
         both carry .sidebar-articles-label-wrapper (:1198) and get all three from
         it. The accordion label declared display and justify-content but not
         align-items, so it was the one real dependency; it is a fixed
         `height: 4.5rem`, so losing the centring would have dropped its text to
         the top. `align-items: center` is now explicit on
         .article-accordion-label in css-components.css, where that component's
         structural styles are declared to live.

   The marker rule below stays bare on purpose: hiding the UA triangle is
   something every <details> in the theme wants, and it cannot bleed into
   anything else. */
summary::-webkit-details-marker {
  display: none;
}

/* THESE ARE SECTIONS OF THE RAIL NOW, NOT CARDS. Joy's call, 2026-10-26,
   continuing the flattening the table of contents started: two boxed white
   panels under a flat nav read as a different component bolted on, where Codex
   puts a second and third group in the same rail, each under its own eyebrow.
   So the border, the radius, the white fill and the hover that darkened the
   border are all gone, and the heading takes the eyebrow treatment below.

   What stays is the icon and the chevron -- they are the only thing marking
   these as openable, and with the box gone they are doing that work alone.

   This also takes two more single-tier legacy primitives off the page:
   --clr-neutral-40 and --clr-white have no dark tier, so the panels were
   painting a white box in dark mode.

   `overflow: hidden` stays, and what it is for has changed twice. It was
   described here as clipping the content during the cards' max-height
   open/close; that pair is gone (2026-10-29) and the clip for all three
   sections is now declared on ::details-content, where the grid row collapses.
   It is kept for the reason given at the declaration below instead.

   The `transition` here went with the border it was delaying: it read
   `border 0ms 400ms linear`, which held the border change back until the panel
   had finished opening. With no border there is nothing to hold back, and its
   max-height half never applied to this element in the first place. */
.sidebar-article-dropdown {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* THE PITCH BETWEEN THE THREE SECTIONS, and 1rem is signed off. Joy asked for
     the sections "a little closer to each other" on 2026-10-27, so this was
     expected to come down -- I held it rather than guessing, because the ask
     arrived in the same breath as "uniform" and uniform was the half that could
     be verified. Once all three sections shared one value she previewed it and
     said the spacing is perfect, so the original complaint was the UNEVENNESS,
     not the amount. Don't reduce it on the strength of that quote. */
  margin-block-start: 1rem;
  /* THIS ELEMENT IS THE NEAREST CLIPPER, which is why the bleed has to be here
     too and not only on the summary inside it (2026-10-27). `overflow: hidden`
     two lines up clips at THIS box's padding edge, so the summary's negative
     margin was being cut off here before it ever reached the room .sticky had
     been given for it -- the fill came out exactly the width of the content box
     and the section glyph ended up flush against its left edge, which is the
     state Joy caught in preview.

     The same pair as the summary and for the same reason: the negative margin
     widens the border box out to .sticky's padding box, the padding puts the
     space back so this element's CONTENT box is unmoved. That last part is what
     makes it safe -- the two article lists are children of this box and their
     own insets are measured off it, so nothing inside the panels shifts.

     `overflow: hidden` IS DELIBERATELY KEPT rather than deleted, even though it
     is vestigial: it reached this component from the bare `details` rule this
     file used to open with (see the tombstone above) rather than being chosen
     for it, and the collapse does not need it -- that is declared on
     `.sidebar-article-dropdown::details-content`, the box whose grid row
     collapses, with
     each panel zeroing its own minimum size inside it. Deleting it would also
     fix the clipping, in
     fewer declarations -- but it would change what this box does to every
     future descendant, to buy an 8px inset. Widening the box changes nothing
     else. */
  margin-inline: calc(var(--rail-bleed) * -1);
  padding-inline: var(--rail-bleed);

  &:hover {
    cursor: pointer;
  }
}

/* TOMBSTONE: `.article-toc + .sidebar-article-dropdown { margin-block-start:
   2rem }` (deleted 2026-10-27, option 2). It was the break between the headings
   and the start of the other sections: the two cards sat a plain 1rem from each
   other, so a wider gap there was what said "different area" without a rule or a
   box to say it.

   IT HAD TO GO BECAUSE THE THING IT SEPARATED NO LONGER EXISTS. That 2rem was
   describing a real boundary -- a flat always-open rail on one side, two
   collapsible cards on the other. Option 2 makes all three the same component,
   so the same declaration stopped marking a boundary and started reading as one
   gap of three being wrong. Joy, in preview: "remove the extra space between in
   this article and recently viewed so everything is uniform."

   All three now take `margin-block-start: 1rem` from .sidebar-article-dropdown
   (:785), with the first zeroed by .article-toc > .sidebar-article-dropdown
   below. Joy has separately asked for that 1rem to come down -- the sections
   should sit "a little closer to each other so it looks like it's all part of
   one long list" -- which is a change to the one shared value and is deliberately
   not made here, so that "uniform" lands and can be seen on its own first. */


/* The first section sits flush to the top of the aside. `margin-block-start:
   1rem` on .sidebar-article-dropdown is a BETWEEN-sections value, and as of
   option 2 the table of contents is one of these too -- so without this it would
   push the whole column down by a rem against nothing, and the three sections
   would no longer be the only things setting the column's vertical rhythm.

   Stated as a child selector on .article-toc rather than :first-of-type for the
   same reason the sibling rule above is: it is legible which one of the three
   this is about, and it does not silently mean a different element if the order
   changes. */
.article-toc > .sidebar-article-dropdown {
  margin-block-start: 0;
}

/* The row's own hover, lifted from .kb-footer-link (css-page-footer.css:281 and
   :320) so the two rails agree. Same three tokens, same transition, and the
   border is declared transparent at rest for the reason written out there: add
   it on hover instead and the label and the chevron each shift by a pixel.

   IT HAS INLINE PADDING SINCE 2026-10-27, and this paragraph used to explain
   at length why it could not. Joy asked for padding inside the hover fill and
   then, separately, for the label to go back to sitting level with "In this
   article" at rest. Those two reads as a contradiction and are not one: the
   padding has to come from the fill growing OUTWARD, not from the content
   moving inward.

   THE OLD REASONING WAS RIGHT ABOUT THE MECHANISM AND WRONG THAT IT WAS A DEAD
   END. It ruled out padding plus a negative inline margin because .sticky
   declared no inline padding, so `overflow-x: hidden` clipped at the very edge
   the row's border box started on and the fill would have been cut off rather
   than widened. True -- but that was a fact about the ancestors, not about this
   row. An overflow clip edge is the PADDING box, not the content box, so an
   ancestor with inline padding leaves exactly that much room for a descendant
   to bleed into, and that is the whole reason this works.

   TWO ANCESTORS CLIP THIS AXIS AND BOTH HAD TO BE WIDENED. .sticky was the
   obvious one and was done first; .sidebar-article-dropdown is this row's
   PARENT and also carries `overflow: hidden`, so for one round the bleed was
   still being cut off at a box nobody had touched, the fill came out exactly
   the content width, and the glyph sat flush against its edge -- the thing
   .sticky's padding was supposed to have prevented. Joy caught it in preview.
   Both now carry the same pair. If this ever regresses, walk every ancestor up
   to .sticky for an `overflow` that is not `visible`; checking only the
   scroller is what went wrong.

   So: `margin-inline` pulls the row's border box out to .sticky's padding box
   on both sides, and `padding-inline` puts the same amount back inside it. Net
   effect on the content is zero -- the glyph and the label sit exactly where
   they did before any of this, level with the eyebrow and the rail -- while the
   fill, the border and the radius all extend a --rail-bleed further out on each
   side. ONE NUMBER, STATED ONCE, on .sticky: if the two ever disagree the
   content shifts, so they are deliberately not two values.

   This is the shape Joy's reference actually has. Codex's site nav puts its
   group headers (`Getting Started`, `Foundations`) flush at the container's
   inline edge with their icons, and indents only the children, behind a rail.
   The three eyebrows in this column line up for the same reason, and the
   heading rows sit indented inside them -- so a row you can open reads as a
   peer of "In this article" rather than as one of its items.

   An earlier pass here indented the glyphs to 12px instead, matching the
   heading rows' text, on the strength of the measured Codex *docs* row
   (`padding: 8px 8px 8px 12px`). That row is a leaf link, not a group header,
   so it was the wrong one of the two to copy. Reverted the same day.

   Symmetrical, unlike the heading rows' 12px/6px pair, because both ends of
   this row carry a glyph -- the section icon and the chevron -- and an
   asymmetric bleed would sit one of them closer to the fill's edge than the
   other. The heading rows can be asymmetric precisely because nothing sits at
   their end.

   THE HEADING ROWS CANNOT HAVE THIS, and the asymmetry is not an oversight.
   Their nearer clipper is the list, not .sticky -- `.table-of-contents` is its
   own scroller with `overflow-x: hidden` and no inline padding of its own -- so
   a bleed on them would still be cut off. Their fill therefore starts at their
   rail. That reads as intended rather than inconsistent: the rail IS their
   inline-start edge, whereas these rows have no rail and their fill spans the
   column.

   Block padding and the 36px floor are still there, which is what gives the
   row a hit area worth aiming at. */
.sidebar-article-dropdown > summary {
  min-height: 2.25rem;
  padding-block: var(--space-3xs);
  /* The pair that widens the fill without moving the content. Both read the one
     value off .sticky; see the long note above. */
  margin-inline: calc(var(--rail-bleed) * -1);
  padding-inline: var(--rail-bleed);
  /* THE ROW'S ONE COLOUR, inherited by the label, the section glyph and the
     chevron alike -- see the note on .sidebar-articles-label h3 for why it
     cannot live on the h3. --neutral-text-soft is the same value the eyebrow
     had, and the same one the heading rows (:593) use -- so all three groups
     agree at rest. As of option 2 this rule IS where the table of contents gets
     it; .article-toc-label is a tombstone (:514). */
  color: var(--neutral-text-soft);
  border: var(--border-width-regular) solid transparent;
  border-radius: var(--radius-sm);
  transition:
    background-color var(--motion-duration-control) var(--ease-in-out),
    border-color var(--motion-duration-control) var(--ease-in-out),
    color var(--motion-duration-control) var(--ease-in-out);

  /* FOCUS TAKES THE HOVER TREATMENT WITH IT -- Joy, 2026-10-29: "when tabbing to
     details title (In this article, recently viewed, etc) can you have the hover
     state show as well as the focus border?" So a tabbed row and a pointed-at
     row look the same, and the ring is what the keyboard adds on top rather than
     the whole of what it gets.

     THE 1px BORDER THIS BRINGS WITH IT IS NOT A DOUBLED RING. It sits outside
     the inset ring, at --neutral-border-alpha against --form-focus-ring, and it
     is the same pairing the reference has -- Codex's nav row carries
     `hover:border-neutral-alpha` and takes its focus outline on top of it. The
     doubling the reset's focus note warns about is two cues of the SAME colour
     tracing the same shape, which is what `outline: none` below is for.

     Grouped rather than given its own block because every declaration is shared;
     see the heading rows, where the two states are deliberately split instead
     because hover must not touch their rail. */
  &:hover,
  &:focus-visible {
    background: var(--neutral-bg-component-soft-active);
    border-color: var(--neutral-border-alpha);
    color: var(--neutral-text);
  }

  /* Press continues onto the next background step, as the footer row does. */
  &:active {
    background: var(--neutral-bg-component-active);
  }

  /* THE FOCUS RING, and until 2026-10-29 this row had none it could show. Joy:
     "when In this article, recently viewed, and related articles are collapsed
     they are not accessible by keyboard." They are reachable -- a <summary> is
     natively focusable and Tab does land here -- but nothing was drawn, so the
     row read as skipped.

     WHY NOTHING WAS DRAWN, and it is geometry rather than cascade. The only cue
     was the sitewide `*:focus-visible` outline in css-reset.css, which paints
     OUTSIDE the border box at an offset. This row deliberately bleeds its border
     box out to the padding box of both its clippers -- the `margin-inline` /
     `padding-inline` pair above, against `overflow: hidden` on
     .sidebar-article-dropdown and on .sticky -- so on the inline axis there is
     nothing left for an offset ring to paint into. On a COLLAPSED panel the
     block axis finishes the job: the summary fills the dropdown, so the ring's
     top and bottom edges fall outside that box too and all four sides are
     clipped, which is the "not accessible by keyboard" report. On the open table
     of contents the bottom edge lands over the list and survives on its own --
     the stray blue line under "In this article" in Joy's screenshot.

     AN INSET RING CANNOT BE CLIPPED, because it paints inside this row's own
     padding box, and it follows `border-radius` exactly -- so the ring is the
     same rounded shape as the hover fill. That shape is what Joy chose on
     2026-10-29, from Codex's side navigation: a --border-width-heavy
     --form-focus-ring ring at --radius-sm around the row, rail left alone.
     Codex draws it with a plain outline at `outline-offset: 2px`, which it can
     because nothing clips its nav; the shape is what was picked, and an inset
     ring is the only mechanism that can draw it here.

     `outline: none` is paired with it for the reason the share trigger's ring
     gives, and here it also removes the stray line: without it the row draws
     both cues at once. NO TRANSITION, deliberately -- the `transition` above
     names its three properties and box-shadow is not one of them. Codex's own
     focus-visible rule sets `transition: none`, and a focus cue that fades in is
     a focus cue that is briefly absent. */
  &:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 var(--border-width-heavy) var(--form-focus-ring);
  }
}

.sidebar-articles-label {
  position: relative;
  display: flex;
  gap: 0.5rem;
  align-items: center;

  /* Weight only, because this class is also on the share trigger, which sets
     its own size at :1473. The eyebrow treatment is scoped to the dropdowns
     below rather than declared here for the same reason: uppercase and tracking
     are not font-size, so the share rule would not have overridden them and
     "Share" would have become an eyebrow too. */
  h3 {
    font-weight: var(--font-bold);
  }

  &:hover {
    cursor: pointer;
  }
}

/* THE EYEBROW FOR ALL THREE SECTIONS as of option 2 -- --step--1 at
   --font-bold, uppercased here rather than in the string, since all three
   headings are `dc` items. It used to share the job with .article-toc-label,
   now a tombstone at :514. Three groups in one rail, introduced the same way. */
/* NO `color` HERE, AND THAT IS THE FIX, not an omission (2026-10-27). It
   carried `color: var(--neutral-text-soft)` and that is what made the glyphs
   disagree with their label: the section icon is a *sibling* of this h3 inside
   .sidebar-articles-label, and the chevron is a sibling of the label itself, so
   neither was ever inside this declaration's subtree. They inherited whatever
   colour reached the summary while the text pinned its own, and Joy reported the
   mismatch.

   The colour now lives on the summary (:932) and all three things -- label,
   section glyph, chevron -- inherit the one value. Font Awesome's SVG+JS
   glyphs paint with `fill: currentColor`, so inheriting is all they need.

   The second thing this fixes is that the summary's `:hover { color }` was
   half-dead: an inherited colour loses to a descendant's own declaration, so
   hovering brightened the glyphs and left the text where it was. One colour per
   state on the summary is the only arrangement where the row changes as a
   unit. */
.sidebar-article-dropdown .sidebar-articles-label h3 {
  font-size: var(--step--1);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* The section glyphs. The kit gives its svg `height: 1em`, so font-size is the
   size control -- the same lever the callouts use.

   --step-0 rather than the eyebrow's own --step--1: at 12.5px these read as
   specks, and in the reference the glyph is visibly larger than the label it
   sits beside. One step up is the smallest difference that still reads as an
   icon. This was --step-2 while the heading was --step-2 and came down with it.

   Scoped to .sidebar-article-dropdown deliberately. `.sidebar-articles-label` is
   also on the share trigger, which sizes its own h3 at --step--1 from
   .article-share-label > h3 (:1624 -- it beats .sidebar-articles-label h3 at :963
   on source order at equal specificity, both being 0,1,1; this was
   .article-share-label-wrapper h3 until 2026-10-27, when the trigger became a
   Codex badge and the wrapper stopped carrying type at all). A blanket rule here
   would blow that glyph up to --step-2; the badge pins it at 1em instead
   (:1641), which is the 12px square the component draws.

   `flex: none` because the parent is a flex row: a replaced element with no
   intrinsic size is shrinkable, and the Related articles label is the one long
   enough to wrap and squeeze it.

   THE 1em SQUARE IS WHAT LINES THE THREE LABELS UP (2026-10-27, with the third
   section). The kit sizes its svg `height: 1em` and lets the WIDTH follow the
   glyph's own aspect ratio, so three different glyphs give three different
   widths -- fa-book, fa-clock-rotate-left and fa-tag are a 448, a 512 and a
   448-unit viewBox -- and with a fixed `gap` beside them the three headings
   started at three different x positions. That is invisible on one row and
   obvious on three stacked ones, which is why it only surfaced now.

   The third glyph was fa-handshake at 640 units, the widest of the three, until
   Joy swapped it for fa-tag on 2026-10-29. The swap needed no change here,
   which is the point of pinning the box: two of the three now happen to agree
   at 448 and the rule is still what holds the column, so the next glyph can be
   any width without a second look.

   Pinning the BOX rather than the glyph: `width: 1em` fixes the column the icon
   occupies and the svg still draws at its own ratio inside it, so nothing is
   distorted -- wide glyphs simply sit tighter in the box than narrow ones. FA
   ships `fa-fw` for precisely this and it is NOT usable here: it is a webfont-CSS
   utility and this theme loads none, the same reason the old `fa-lg`/`fa-fw`
   pair came off the markup on 2026-10-26 (article_page.hbs:133).

   This also makes the rail indent a statable number instead of a per-glyph one:
   the icon's centre is now 0.5em from the column's content edge for all three
   sections, whatever glyph is in it. */
.sidebar-article-dropdown .sidebar-articles-label > svg {
  font-size: var(--step-0);
  width: 1em;
  flex: none;
}

/* The chevron, matched to the glyph on the other end of the row. Pinned rather
   than left inheriting: with the h3 beside it now at --step--1, an inherited
   size is whatever the summary happens to be, which is not related to either
   thing it is lining up with. */
.sidebar-article-dropdown > summary > span[aria-hidden='true'] > svg {
  font-size: var(--step-0);
  transition: all 300ms ease;
}

.recent-articles,
.related-articles {
  /* THE max-height PAIR THAT USED TO ANIMATE THESE TWO PANELS IS GONE
     (2026-10-29) and the ::details-content rule below now animates all three
     sections the same way. It animated `max-height: 0 <-> 25rem` with a
     matching 400ms on each state, and Joy caught the symptom in preview:
     "either snapping or open, or seems to be running on a different transition
     timing than in this article and related articles."

     WHY A CAP CANNOT KEEP TIME. max-height only moves the box while it is below
     the content height; past that the box is already its full size and the
     remaining animation is spent on a number nothing can see. So the VISIBLE
     duration is the declared duration times (content height / cap), and the
     rest of it is a dead pause -- after the open, and before the close, because
     the ramp runs the other way. A per-frame recording of the shipped build
     against 25rem = 400px:

       section            content   moves for   then still   close: still first
       Recently viewed     156px      109ms       290ms           166ms
       Related articles    301px      234ms       170ms            45ms

     Same declaration, 27% of it visible in one panel and 59% in the other --
     which is exactly the "different timing" she was seeing, and the 166ms of
     nothing before Recently viewed moved is the "snap". Nothing about it was
     tuned wrong: the fraction is set by how many rows a list happens to have,
     so Recently viewed drifts as a reader's history grows and no duration
     written here could have held still.

     The replacement needs no cap, so it cannot drift -- and it also retires the
     second defect this one had, that a list taller than 25rem was clipped with
     no scrollbar and no way to reach the rest.

     `overflow: hidden` STAYS AND ITS JOB HAS CHANGED, so it is not vestigial
     here the way it is on `.sidebar-article-dropdown`. It clipped the max-height
     before; now it is what zeroes this element's automatic minimum size as a
     grid item, and a 0fr row cannot collapse past an item with a content-based
     minimum -- i.e. without it the sections would not close at all. Same
     mechanism `.table-of-contents[data-spy]` gets from its own scroller. */
  overflow: hidden;
  /* NO INLINE PADDING, and that is what lets these rows share the heading
     list's rail line. It was `padding: 0 10px` until 2026-10-27. The rail is a
     border on each row's inline-start edge, so the row's border box has to
     start at the same place the heading rows' do -- and theirs start at the
     column's content edge, because `.table-of-contents` sets no inline padding
     and css-reset zeroes the <ol> (css-reset.css:239). Ten pixels here would have offset
     this group's rail from that one by exactly ten pixels, which is the one
     thing that would stop the three groups reading as a single column.

     THE 10px WAS LOAD-BEARING AND ITS JOB HAS BEEN TAKEN OVER, so check the
     replacement before putting it back. This element is `overflow: hidden`, so
     the clip edge is now the content box, and the sitewide focus outline paints
     ~4px OUTSIDE a row's border box -- it would be cut off on the inline-start
     edge. The rail answers that: on focus it takes --form-focus-ring, which is
     the same mechanism the heading rows use (:643) for the same reason, and a
     border paints inside the border box where nothing can clip it. */
  padding: 0;
  /* NOT TRANSITIONED, and it never was in effect: the `border 0ms 400ms linear`
     that used to sit under this and the `border 0ms linear` on the [open] rule
     both came off with the max-height pair. Neither state ever gave the border
     a second value -- nothing else in any stylesheet touches it either -- so
     there was no change for a delay to hold open. It is kept as a declaration
     because the 2px is layout: it is what the rows are measured off. */
  border: 2px solid transparent;
}

/* THIS IS WHAT MAKES THE PANELS ANIMATE SHUT, AND IT ANIMATES NOTHING ITSELF.
   Joy, 2026-10-27: "when you click to open the section, there is a nice
   transition to open, when you close it snaps shut" -- and she had been trying
   to fix it for a while.

   THE HEIGHT TRANSITION WAS NEVER THE PROBLEM, under either mechanism. Both
   states declare the same duration, so the close is correctly specified and
   should run. What stopped it is <details> itself: removing the `open`
   attribute un-renders the element's children in the same frame, so the panel
   goes from rendered at full height straight to not rendered, with no
   intermediate state for the transition to interpolate. The animation was not
   too fast -- the thing being animated was gone before it could start. Opening
   works because the attribute lands first, the children become rendered at
   zero height, and the transition then has something to run on. So no
   duration, easing or height change could ever have fixed this, which is why
   it resisted every attempt from that direction.

   ::details-content is the box the UA wraps everything-but-the-summary in, and
   `content-visibility` is the property that flips. It cannot be interpolated --
   but `allow-discrete` defers the flip to the END of the transition when moving
   to the not-rendered value, so the subtree stays rendered for the full 400ms
   while the row animation beside it runs down. The row animation does all the
   visible work; this only buys it the time it was never given. On open the flip
   to rendered happens at the start, so that direction is untouched.

   THE TWO 400ms IN THIS RULE MUST MATCH EACH OTHER. Too short on the
   content-visibility half and the panel vanishes mid-animation; too long and
   there is a dead pause after it has finished closing. They are the last two
   400ms literals in the component now that the cards' four have gone, and
   binding the pair to one custom property is still worth doing -- deliberately
   not in the same change as a mechanism swap, so a revert is one rule.

   NO @supports GATE AND NO JS FALLBACK, deliberately. If an engine does not
   support ::details-content the selector matches nothing, the subtree is
   un-rendered instantly as before, and the behaviour is exactly what shipped
   before this rule -- so the degradation costs nothing relative to today and
   needs no second code path. The JS route (intercept the toggle, animate, drop
   `open` on transitionend) was rejected on that basis plus one more:
   article-nav-seek.js already carries three jobs in a fixed-order rAF, and
   attribute juggling is how that file stops being reasonable to read.

   ONE MECHANISM, ALL THREE SECTIONS (2026-10-29). This rule was the
   content-visibility line alone and the grid below was scoped to .article-toc,
   because the table of contents was the only panel that could not use the
   cards' max-height cap. It turned out to be the only one the cap suited
   either -- the recording is written out on `.recent-articles` -- so the grid came
   up here and the two cards came off max-height entirely. The selector is now
   deliberately unscoped: a fourth section added to this column inherits a
   working accordion with no rule of its own, which is the opposite of how the
   cards and the table of contents each ended up with their own.

   THE TABLE OF CONTENTS IS WHY IT IS THIS MECHANISM AND NOT A CAP, and that
   reasoning is what generalised (2026-10-27). Its open height is whatever the
   flex chain gives it -- up to the whole viewport on a long article -- so a
   25rem cap would silently truncate the one panel in the column that is allowed
   to be tall, and any single value picked here would be competing with
   fitColumn() in article-nav-seek.js for the same job.

   0fr <-> 1fr IS THE TRICK, and the reason it is the right one is that it needs
   no number at all. An `fr` row resolves against whatever space the container
   has: auto-height while the column is roomy, and the flex-shrunk height once it
   is not -- so `1fr` is the correct open height in both regimes without this
   rule knowing which it is in. `height: 0 <-> auto` would be the obvious
   alternative and is rejected: it needs `interpolate-size: allow-keywords`,
   which is not close to the support ::details-content itself has.

   ONE ROW MEANS ONE CHILD, which holds for all three sections and is worth
   knowing before a fourth: `{{recent_articles}}` and `{{related_articles}}`
   each render a single <section> wrapper, and the table of contents is a single
   <div>. A second element at this level would land in an IMPLICIT row, which
   `grid-auto-rows` leaves at `auto` -- it would not collapse, and the section
   would close down to the height of that child instead of to nothing.

   `overflow: hidden` IS NOT OPTIONAL, and the first version of this rule left it
   out on the argument that it was already covered. That argument: a grid item's
   automatic minimum size is what normally stops a 0fr row collapsing, and it is
   zero for the table of contents anyway, because .table-of-contents[data-spy]
   declares non-visible overflow for its own scroller. True, and not enough --
   it only makes the ITEM collapsible, and says nothing about what the item
   paints. The <ol> inside carries `padding-block: var(--space-2xs) 0` (:566)
   for the gap under the header, and a margin or padding on the grid item
   overflows a zero-height area rather than being absorbed by it, so a closed
   section showed a strip of dead space under its header -- precisely the
   uniform spacing Joy had just asked for. Declaring the clip here fixes that
   and makes the collapsibility explicit instead of a property of a scroller two
   levels down that nothing stops someone changing.

   EACH PANEL STILL OWES THE ITEM HALF OF THAT, and the clip here does not cover
   it: an item with a content-based minimum size holds the 0fr row open however
   this container is clipped. The table of contents gets a zero minimum from its
   scroller; the two cards get it from the `overflow: hidden` they already
   carried for the max-height, which is the one declaration of that pair worth
   keeping and is flagged as load-bearing there. Any fourth panel has to bring
   its own, or it will simply refuse to close.

   Below 60em the table of contents does not exist, because .sticky is
   `display: none` (:20). The two cards do -- they render in the mobile flow --
   so this rule is no longer sticky-column-only the way it was when it was
   scoped to .article-toc.

   content-visibility IS RESTATED, not inherited, even though the rule above is
   the same selector plus a pseudo-class: a whole `transition` replaces rather
   than extends, so dropping that half from either rule would cost exactly the
   close animation the whole mechanism is for. */
.sidebar-article-dropdown::details-content {
  display: grid;
  grid-template-rows: 0fr;
  overflow: hidden;
  transition:
    grid-template-rows 400ms ease-out,
    content-visibility 400ms allow-discrete;
}

.sidebar-article-dropdown[open]::details-content {
  grid-template-rows: 1fr;
}

/* THE CHEVRON TURNS FASTER THAN THE PANEL OPENS, 200ms against 400ms, and that
   is signed off rather than unnoticed. I flagged the mismatch twice as a
   possible defect; Joy, 2026-10-27: "not sure what you mean about the chevrons,
   but it looks good to me." Which is the useful answer -- the thing it would
   have cost to match them is that the chevron is the click's acknowledgement
   and the panel is the result, so a chevron that finishes early makes the
   control feel responsive while the content is still arriving. Leave the two
   durations different, and do not fold this one into any shared property. */
.sidebar-article-dropdown[open] summary > span[aria-hidden='true'] > svg {
  rotate: 180deg;
  transition: 200ms ease-out;
}

.sidebar-articles-label-wrapper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
}

.related-articles,
.recent-articles {
  & ul {
    /* The gap between a section's header row and its first child, the same in
       all three groups. It matched `.article-toc-label`'s margin-block-end when
       it was written; that class is gone (tombstone at :514) and the dependency
       now runs the other way -- the table of contents copies THIS declaration
       (:566), because Joy's verdict on the spacing was "Recently viewed is
       correct."

       THIS REPLACED A 1rem/1rem PAIR WHOSE BOTTOM HALF WAS A FOCUS FIX, not a
       spacing choice -- the panel was `overflow: hidden` with inline padding
       only, so the last row's outline was clipped at the bottom and the padding
       was there to make room for it. That fix is obsolete rather than deleted,
       and it stayed obsolete through the 2026-10-29 focus pass even though the
       ring came back: the cue is drawn INSIDE the row now -- the rail until then,
       a positioned pseudo-element since -- so no clipper can reach it and the box
       does not have to be opened up to accommodate one. Nothing is lost by
       closing it, and the three lists end flush with each other. */
    padding-block: var(--space-2xs) 0;
    display: flex;
    flex-direction: column;
    /* The rail lands under the middle of the section icon -- same declaration as
       the heading list (:572), reading the one value declared on .sticky, where
       the derivation is written out. */
    margin-inline-start: var(--rail-indent);
    /* NO gap, for the same reason the heading list has none (:576): the rail is
       a border on each row, so the rows must be contiguous or the rule breaks
       into dashes with a 1rem hole at every row. Row rhythm comes from the
       anchors' own padding-block instead. This is also the "tighten the
       expandable sections" change Joy asked for on 2026-10-26 -- it was always
       going to arrive with the rail rather than before it, because the two are
       the same decision. */
    gap: 0;
    /* A `transition: max-height 400ms ease-out, border 0ms linear` sat here
       until 2026-10-29 and had never done anything: it is a copy of the card
       rule's declaration and this <ul> has neither a max-height nor a border
       that changes, so both halves referred to properties that never moved on
       this element. It came out with the real pair on `.recent-articles`
       rather than
       being left to look like part of the mechanism. */
  }
  /* THESE ARE RAIL ROWS, NOT PROSE LINKS, and until 2026-10-27 they were the
     only thing in this column still styled as the latter: plum
     (--clr-secondary-140) at --step-1 bold, underlined at rest, with a pale
     plum hover fill. The treatment below is `.table-of-contents a`'s (:579)
     ported wholesale, so all three groups in the rail now agree.

     TWO REASONS, AND THE FIRST IS NOT A MATTER OF TASTE. The plum was a local
     override of the sitewide link colour, which became a Codex role on
     2026-10-24 -- and css-reset.css:345 records Joy's reason for that change
     ("otherwise we're going to keep coming across the purple links everywhere
     we go") along with the finding that fixing it per page was the wrong place.
     This was one of the pages still doing that. --clr-secondary-140 also has no
     dark tier at all, so these rows were a dark-mode defect waiting to be
     found, not just an off-brand colour.

     The second: the sitewide link treatment would be wrong here even now that
     it is on-brand. It is body text plus an underline, which is right for an
     inline link in prose and wrong for a nav list, where every row would be
     underlined and the column would read as a paragraph of links. Codex's nav
     rows are quiet and unadorned for the same reason. So these take the
     neutral rail treatment rather than the document's link treatment.

     THE GEOMETRY FOLLOWED IN THE NEXT PASS, same day, and this paragraph used
     to say it had deliberately been left out. It had: treatment first so each
     half could be judged on its own. The rail, `gap: 0`, the row padding and
     the panel's own inline padding all landed together, because they are one
     decision and not four -- a railed list has to be contiguous (see the <ol>
     note at :576) and its rows have to start on the same line as the heading
     rows' or the two groups read as separate columns. */
  a {
    /* Block-level so the row, its rail and its padding span the full column.
       The anchors sit inside <li>, which stretch as column flex items, but an
       inline anchor would only be as wide as its text -- the rail would still
       draw, at the right height, against a box that stopped mid-row. */
    display: flex;
    /* Containing block for the focus ring below, same as the heading rows. */
    position: relative;
    /* THE RAIL, and it is the same declaration as the heading rows' (:586).
       Drawn on every row so the per-row borders join into one continuous rule
       down the list -- which is only true because the <ul> has `gap: 0`. */
    border-inline-start: var(--border-width-heavy) solid
      var(--neutral-border-soft);
    /* The heading rows' values exactly. Asymmetric inline: the start pays for
       the rail and the indent, the end only needs to keep text off the column
       edge. This padding is also the row rhythm now that the gap is gone. */
    padding-block: var(--space-2xs);
    padding-inline: var(--space-2xs) var(--space-3xs);
    font-size: var(--step-0);
    font-weight: var(--font-regular);
    line-height: 1.3;
    color: var(--neutral-text-soft);
    text-decoration: none;
    /* Same reason as the heading rows: these are author-written article titles
       in a 256px column, and they are likelier to be long than a heading is. */
    overflow-wrap: break-word;
    /* THE INK IS THE WHOLE HOVER. No fill, no underline change, nothing else.
       Joy, 2026-10-27: "on hover they need to take the brand color treatment
       (like all of our other links)", then "remove the background color on
       hover -- just the color needs to change."

       --brand-text is the sitewide link hover set on 2026-10-25 and the
       breadcrumb trail's colour; css-reset.css:368 holds the reasoning. #992008
       light, #fdb5a6 dark, both tiers -- raised one vermillion step on
       2026-10-28 for AAA, see the token's own note. This rail is one of the
       three places a hover lands on a mid-tone surface, so it is the AA
       exception documented there (6.71:1 light, 6.44:1 dark) rather than the
       8.12:1/8.58:1 the page surfaces get. So this is now the sitewide link model
       exactly, including the death of the background tint that the same
       decision called for -- these rows had been the last holdout.

       I HAD KEPT A FILL HERE FOR ONE ROUND AND THE ARGUMENT FOR IT WAS WRONG.
       It ran: the fill is this column's row-locator, both groups above use one,
       so dropping it makes these the only rows in the rail without. True as
       description, but it treats "matches the rail" as the goal when the thing
       being matched is the link model -- and a brand-ink hover is already an
       unambiguous row-locator on its own. A tint underneath it was belt and
       braces, and it made the hover louder than any other link in the theme.

       DELIBERATE SPLIT FROM THE HEADING ROWS ABOVE, which still hover to
       --neutral-text with a fill and were left alone. These rows are
       destinations -- they navigate to another article, like any other link in
       the theme. The heading rows are in-page jumps inside the article you are
       already reading, so they stay part of the page's own furniture.

       CONFIRMED BY JOY, 2026-10-27, AND CLOSED. I raised the split twice as a
       possible inconsistency, on the grounds that three identical-looking lists
       in one rail with two different hovers reads as an oversight. Her answer
       was "that is what I want". So the two treatments are the decision, not a
       loose end -- do not unify them, in either direction.

       FOCUS IS NOT GROUPED WITH HOVER, as of 2026-10-29. Joy: "I'd like you to
       remove the hover state in focus on the article links in the recently
       viewed and related articles details sidebars." The two were grouped here
       from the day this treatment landed, and the ring below was the only thing
       keeping that honest -- a focused row took the brand ink AND the ring while
       a focused heading row took the ring alone, so the three groups disagreed
       in a way nothing had asked for.

       THE RULE THIS SETS, and it is Joy's, same message: the ring is the focus
       cue and a hover treatment on top of it is emphasis to be asked for, not
       assumed. The three summary rows above are the one place she asked for it.
       Hover and focus stay in separate blocks here even where a declaration
       would be shared, so neither can quietly pick the other's up again.

       `cursor: pointer` GOES WITH HOVER AND ONLY HOVER, which is not an
       oversight either: there is no pointer to restyle during a keyboard focus.
       The declaration is inert on this element in any case -- the anchors carry
       an href, so the UA already gives them a pointer -- and it is left where it
       is rather than deleted because that is true of the heading rows' copy too
       and the pair of them is one cleanup, not this change. */
    &:hover {
      color: var(--brand-text);
      cursor: pointer;
      text-decoration: none;
      transition-duration: var(--transition-duration);
    }

    /* THE SAME PSEUDO-ELEMENT RING AS THE HEADING ROWS, and ported wholesale for
       the same reason the rest of this treatment was: all three groups in the
       rail agree. The reasoning lives at the heading rows' `:focus-visible` --
       why not the sitewide outline, why not an inset box-shadow, why the
       inline-start inset is one rail width, and the tombstone for the rail
       recolour this replaces. Only the clipper differs: here it is the panel's
       own `overflow: hidden`, which has no inline padding to spare since that
       came off, rather than the table of contents' scroller.

       THE RING IS NOW THE WHOLE OF WHAT FOCUS DRAWS HERE, which it was not when
       this block was first written: the --brand-text ink step was shared with
       hover for one round and Joy removed it the same day. See the hover block
       above for the rule that came out of it. So all three groups in the rail
       show one cue on focus and the same one, and the hover split Joy confirmed
       on 2026-10-27 -- brand ink here, --neutral-text plus a fill on the heading
       rows -- is a hover split only, not a focus one. */
    &:focus-visible {
      outline: none;

      &::after {
        content: '';
        position: absolute;
        inset: 0 0 0 var(--border-width-heavy);
        border: var(--border-width-heavy) solid var(--form-focus-ring);
        border-radius: var(--radius-sm);
        /* Decoration over a row that is entirely a hit target. */
        pointer-events: none;
      }
    }
  }
}

h2.recent-articles-title,
h2.related-articles-title {
  display: none;
}
/*************************/

/* Article Contaainer */

.article-wrapper {
  gap: 2rem;
  display: flex;
  flex-direction: column;
}

@media (min-width: 45em) {
  .article-wrapper {
    z-index: 0;
  }
}

/* The reading container. Card treatment is the one `#algolia-facets` carries on
   the search results page (css-page-search-results.css:435), which is also the
   home page's card: neutral/bg, 1px neutral/border-soft, radius/md, and a
   0 2px 6px shadow/regular drop shadow. Joy asked for parity with that card on
   2026-10-27, both tiers.

   All four roles are semantic and all four are defined in `.dark-mode`
   (css-brand-tokens.css:1032, :1048, :1087), so this needs no dark-mode rule --
   which is the whole reason to adopt them. What it replaces did need one and
   never had one: --card-border is `1px solid var(--clr-neutral-40)`, a legacy
   primitive fixed at #c2c2c2 in both tiers, so the border was a light grey
   hairline on a #10171e page.

   `color` is declared even though the inherited ink was already correct, because
   a box that owns its own fill has to own the ink on it -- otherwise the two can
   be changed apart. --neutral-text is #10171e / #ebf0f4; the dark step is a
   light step of the hue and deliberately not pure white.

   One number is NOT the card's and is kept on purpose: the 2rem padding (the
   card's `1.25rem 1rem` is sized for a facet list, not for a column of running
   text). The duplicated `padding: var(--gap-sm)` that used to head this block
   was dead -- the 2rem below it won -- and is deleted rather than reinstated.

   NO MAX-WIDTH, AND THERE IS NOTHING FOR ONE TO DO HERE. This carried
   `max-width: 70em` (1120px) until 2026-10-27, when it measured as unreachable
   at every viewport and came out at Joy's go-ahead. This box is a block child
   of .article-wrapper, which is the `flex-grow: 999` content column of
   `.sidebar`, so its widest used size is the content track less the aside and
   the gap: 71.5rem - 16rem - 2rem, about 855px against a 1120px cap. Below 60em
   `.sticky` is `display: none` and the wrapper takes the whole track, but that
   range sits under a 960px viewport, so the cap did not bind there either.

   It went because a cap that governs nothing today would wake up silently if
   the sidebar ever goes or the track ever widens, and because it was a third
   page width competing with --content-max-width (71.5rem). If this page ever
   does want a reading-measure cap, .article-wrapper is the place for it -- one
   declaration holding both cards to the same width, rather than the number
   being restated on each box. Joy, 2026-10-27, on never seeing the two cards
   hold different widths responsively: that observation was right, and the 24px
   drift this file used to claim could not happen. */
.article-container {
  padding: 2rem;
  border: var(--border-width-regular) solid var(--neutral-border-soft);
  border-radius: var(--radius-md);
  background-color: var(--neutral-bg);
  color: var(--neutral-text);
  box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
    var(--shadow-regular);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* THE ARTICLE META, moved off the legacy primitives 2026-10-27 with the vote
   prompt and for the same reason: --clr-neutral-160 is #333333 in css-root.css
   with no dark tier, so the tags and the date were painting #333333 on the
   #1e2a35 card at 1.16:1. Found while fixing the vote label, above the fold on
   the same page.

   --neutral-text-soft, not --neutral-text, because both of these are secondary
   annotations rather than body copy -- the same call as the vote tally at :2557.
   NOTE THAT THIS IS NOT A PURE LIKE-FOR-LIKE IN LIGHT MODE: #333333 goes to
   #4f5f63, so the tags and the date get a step lighter against the card
   (12.52:1 to 6.61:1, still comfortably AA). That is the intended hierarchy --
   metadata should sit below the prose it labels -- but it is a visible change in
   light mode as well as a fix in dark, so it is called out rather than buried. */
.article-page {
  .tags {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    color: var(--neutral-text-soft);
  }

  .last-updated {
    display: block;
  }

  .metadate {
    color: var(--neutral-text-soft);
  }
}

.img-50 {
  width: 50%;
  display: flex;
}

.img-75 {
  width: 75%;
}

/***************************/

/* Article Meta */

/* THIS BOX NOW RESERVES SPACE AND NOTHING ELSE, since Joy had the top vote block
   removed on 2026-10-29 and .article-share is `position: absolute`. An
   absolutely positioned child contributes no height, so the one remaining child
   cannot hold this container open -- the vote block was the only `static` one,
   and it was contributing every pixel: measured in preview at 40px tall inside a
   40px container, with the 40px trigger starting at the same top.

   Left alone, this collapsed to zero and the article title rose 40px into the
   trigger, which would have overlapped the two exactly. Joy's instruction was
   that the share button stays where it is, so the height is reserved here rather
   than the trigger being moved or put back in flow -- it is absolute so that an
   open .share-dropdown overlays the article instead of pushing it down, which is
   a separate thing worth keeping.

   THE NUMBER IS THE TRIGGER'S OWN HEIGHT, written as the arithmetic rather than
   as 2.5rem so it is checkable against the control it is reserving for:
   .article-share-label's `padding-block: 0.75rem` twice, its
   --border-width-regular border twice, and its --font-size-sm label at
   `line-height: 1`. Every one of those is FLAT -- 14px and 1px are literals in
   css-brand-tokens.css and the padding is in rem -- so this resolves to a stable
   40px and does not drift with the viewport the way a --step-* value would. It
   will not follow the trigger automatically if that control is ever restyled;
   the arithmetic is here so the mismatch is obvious when it is.

   TOMBSTONE: `justify-content: space-between`, which positioned the vote block
   against the trigger. With no in-flow children left there is nothing for it to
   distribute. `display: flex` is kept deliberately: an abs-positioned child's
   static position is the container's content-box start under either display, so
   this is neutral today, and the migration-assistant section may yet put real
   children back in here. */
.article-meta {
  display: flex;
  flex-direction: row;
  min-block-size: calc(
    (0.75rem * 2) + var(--font-size-sm) + (var(--border-width-regular) * 2)
  );
}

/* The <details> shell. Its --clr-white fill moved down onto .article-share-label
   on 2026-10-22, where it was always doing the work: this element is a
   positioning wrapper that holds the trigger *and* the open panel, and the panel
   now paints itself on the semantic tier. A white fill here would have shown
   through as a strip across the panel's 0.5rem top margin -- invisible in light,
   a white seam under the trigger in dark.
   The light-pill-opening-a-dark-panel mismatch that note flagged as "a design
   question rather than a bug" is CLOSED as of 2026-10-27: the label is a Codex
   Codex button on the semantic tier (:1592), so trigger and panel now answer to the
   same theme and both of this control's entries came off the pinned-light list. */
.article-share {
  z-index: 999;
  position: absolute;
  inset-block-start: auto;
  margin-block-start: 0;

  /* These were inherited from bare `details` rules until 2026-10-26 and are
     declared here now -- see the note at :701 for the full accounting. The
     direction is the one that matters: .share-dropdown is in normal flow, not
     absolutely positioned, so a row here would put the open menu beside its
     trigger instead of under it.

     `overflow: hidden` CAME OFF ON 2026-10-27, and it was the third of the three.
     It arrived by inheritance rather than by decision -- nothing here ever needed
     a clip, since this element is a positioning wrapper sized by its own children
     -- and it was costing two visible things:

       - most of .share-dropdown's floating shadow, so the open menu read flatter
         than the header's sign-in menu it is meant to match. Found while checking
         why the two looked different; it is the same card otherwise.
       - the trigger's own resting drop shadow, which arrived with the Codex
         button geometry below and would have been clipped on three sides.

     It also clipped the sitewide focus outline on the summary, which is why that
     control carries an inset ring instead (the long note further down). The ring
     is kept rather than reverted: it works either way now, and swapping back to
     the plain outline is a one-line change if that is preferred. */
  display: flex;
  flex-direction: column;
}

/* THE SHARE TRIGGER IS A CODEX BADGE as of 2026-10-27 -- regular emphasis,
   neutral severity, trailing icon. Joy's call: "For the top I'd like to use the
   badge with icon style", pointing at the Badge component in the Codex Component
   Library (file OH4VrtoZYBoIagz2HRLmJB -- Documentation frame 21302:221,
   Components frame 6872:72933).

   EVERY NUMBER AND ROLE BELOW WAS MEASURED OFF THE RENDERED FRAMES, not taken
   from Figma's codegen. `get_metadata` returns no child ids for either frame --
   the usual sparse result on a Codex doc frame -- so there was no structure to
   read, and all 18 variants were sampled pixel by pixel instead. What that
   established, none of which is guessable from the token names:

     emphasis   background          border                 label
     soft       {sev}-bg            {sev}-border           {sev}-text
     regular    --neutral-bg        {sev}-border           --neutral-text-soft
     strong     {sev}-bg-strong     {sev}-border-strong    --fixed-white, or
                                                           --fixed-black on
                                                           warning and success

   TWO FINDINGS WORTH KEEPING, because a future severity swap depends on them:

     - REGULAR'S LABEL IS --neutral-text-soft FOR ALL SIX SEVERITIES. The ink in
       all six reads #4f5f63. So a regular badge is a neutral chip with a
       coloured edge, not a tinted one, and switching this control's severity
       means changing the border and the icon only.
     - THE ICON IS NOT THE LABEL COLOUR. In soft and regular alike it takes
       {sev}-icon, which is darker and more saturated than the ink beside it --
       here --neutral-icon (#384649) against --neutral-text-soft (#4f5f63).
       Hence the svg's own `color` below; inheriting would wash it out.

   THE BOX IS CODEX'S BUTTON, NOT THE BADGE'S, AND NOT .btn's EITHER. Joy,
   2026-10-27, on seeing the badge built to spec: "I'd like to revert from the
   badge padding/size and go to button formatting but with the border radius of
   the badges... It's just too small." The first pass at that took .btn's own
   --padding and --size from css-buttons.css, which was a guess at what "button
   formatting" meant; she then sent the real thing -- a DevTools capture of the
   Codex site's Button component, `button.kb-button.kb-button-md
   .kb-button-regular`, measuring 148.31 x 40. So these values are the component's
   own, read off its live stylesheet, which is the source of truth for Codex
   (Figma lags it). Transcribed, with Tailwind's `--spacing` of 4px resolved:

     .kb-button-md       font-size --font-size-sm
                         padding-block  calc(--spacing * 3) = 12px
                         padding-inline calc(--spacing * 4) = 16px
     .kb-button-md/-sm   border-radius --radius-round
                         font-family --font-family-primary
                         font-weight --font-weight-strong
                         line-height 1
     .kb-button-regular  background --neutral-bg-component
                         color      --neutral-text
     .kb-button          display inline-flex; align-items/justify-content center
                         gap calc(--spacing * 2) = 8px
                         border --border-width-regular solid
                                --neutral-border-alpha-strong
                         box-shadow 0 --shadow-y-offset --shadow-blur 0
                                    --shadow-regular
                         cursor pointer; transition .2s --ease-in-out

   THE HEIGHT ARITHMETIC CHECKS OUT AGAINST HER MEASUREMENT, which is the proof
   the spacing scale was read correctly: 12 + 12 padding + 2 border + a 14px
   label at line-height 1 = 40px, the tooltip's figure exactly. The badge was
   22px, and the .btn guess ~31px.

   THERE IS NO LONGER ANY DEPARTURE FROM THE COMPONENT. The badge's --radius-sm
   (6px) was carried as a deliberate exception for most of 2026-10-27 -- Joy
   asked for "button formatting but with the border radius of the badges" and
   confirmed it once -- and then she sent the Codex Dropdown component and said
   "let's have this exact style", whose trigger is the pill. So the exception is
   withdrawn and this is --radius-round (9999px), which is what
   .kb-button-md/-sm declares. Nothing here overrides the component now.

   Worth knowing if a 6px corner is ever wanted back: it was only ever this one
   declaration. The rest of the box was already the component's.

   FOUR THINGS THE BUTTON SPEC CHANGED FROM THE BADGE, worth listing because
   three of them are not obvious and one is a bug waiting to happen:

     - THE FILL IS --neutral-bg-component (#deebec), a tinted component surface,
       not the badge's --neutral-bg (the page's own). So this no longer disappears
       into the article card behind it, which is most of why it now reads as a
       control.
     - THE INK IS --neutral-text (#10171e), not --neutral-text-soft (#4f5f63).
     - THE ICON HAS NO COLOUR OF ITS OWN. On a badge the glyph takes {sev}-icon
       and is deliberately darker than its label; .kb-button sets one `color` and
       lets the svg inherit it. So the `color: var(--neutral-icon)` the badge
       needed is GONE from the svg rule below -- keeping it would have made the
       glyph lighter than the label it sits beside, the exact mismatch the note
       at :976 describes on the sidebar headings.
     - THERE IS A RESTING DROP SHADOW NOW, which is why the focus rule further
       down has to restate it. `box-shadow` is one property: a focus rule that
       declares only the inset ring replaces the drop shadow rather than adding
       to it, and the shadow would vanish for as long as the control held focus.
       Both shadows are in one declaration there.

   The shadow is assembled by hand from the primitives because Codex's own
   --shadow-regular composite is a dependency cycle and dead; css-brand-tokens.css:515
   is the full explanation, and this `0 y blur 0 color` shape is the house idiom
   in a dozen files already (css-page-footer.css:430 and css-components.css:1868
   among them).

   Weight: --font-weight-strong is 600, which is the reset's own h3 --font-bold,
   so it is not restated here -- and it settles the open question from the .btn
   pass, where --font-bolder (700) looked too heavy. Codex agrees it is 600.

   Type is --font-size-sm (a flat 14px) rather than this theme's fluid --step-0,
   which floors at exactly 0.875rem = 14px and rises to 14.2px. The flat token is
   what the component declares, css-search.css already uses it for the same
   reason, and with `line-height: 1` the button's height is a function of its
   font-size -- so a fluid value would make a 40px control drift with the
   viewport for no benefit.

   The gap is 0.5rem, which is `--spacing * 2` and also what
   .sidebar-articles-label (:952) sets and what every other icon + label row in
   the theme uses, so the component value and the house value agree.

   WHAT CAME OFF: --clr-white, a hardcoded `1px solid var(--clr-neutral-160)`,
   a 0.25rem radius, --clr-neutral-160 ink and `line-height: 1.5` -- all legacy
   primitives, which is exactly why this control had to sit on the pinned-light
   list in css-utilities.css. Both of its entries there are deleted in this same
   commit, and that is NOT optional: `:root[data-theme='dark'] .article-share-label`
   is (0,3,0) and would beat this rule's (0,1,0), forcing #333333 ink onto a
   #1e2a35 badge -- the precise failure that block exists to prevent, pointing
   the wrong way.

   `justify-content: space-between` went too. This control is content-sized with
   a gap; spreading its two children only did anything because the box happened
   to be wider than they were -- which it was, for the hit-area reason the note
   at the bottom of this section describes. `gap` is restated at the same value
   .sidebar-articles-label (:952) already sets, rather than inherited from it,
   because at equal specificity these two resolve on source order alone and that
   is too quiet a thing to depend on for a visible number. */
.article-share-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding-block: 0.75rem;
  padding-inline: 1rem;
  border: var(--border-width-regular) solid var(--neutral-border-alpha-strong);
  border-radius: var(--radius-round);
  background-color: var(--neutral-bg-component);
  color: var(--neutral-text);
  box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
    var(--shadow-regular);
  font-family: var(--font-family-primary), system-ui, sans-serif;
  font-size: var(--font-size-sm);
  transition:
    background-color var(--motion-duration-control) var(--ease-in-out),
    box-shadow var(--motion-duration-control) var(--ease-in-out);
}

/* BOTH OF THESE HAVE TO BE ON THE h3, and neither is redundant. css-reset.css:258
   sets `line-height: 1.3` on every h1-h4 and NO font-size, and an element's own
   declaration beats an inherited value at any specificity -- the same trap the
   note on .sidebar-articles-label h3 (:976) describes for `color`. So:

     - without `font-size` the h3 keeps the UA's 1.17em and draws ~16.4px inside
       a 14px box;
     - without `line-height` the reset's 1.3 stands, the label occupies 18.2px
       instead of 14px, and a control specified at 40px renders at 44px.

   .kb-button-md declares `line-height: 1` for exactly this reason: it is what
   makes the height a function of the padding and the font-size alone. */
.article-share-label > h3 {
  font-size: var(--font-size-sm);
  line-height: 1;
}

/* NO `color` HERE -- see the third bullet in the note above. .kb-button sets one
   `color` on itself and the glyph inherits it, so the icon tracks the label
   through every state for free. The badge's `color: var(--neutral-icon)` came
   off on 2026-10-27 with the rest of the badge geometry.

   `width: 1em` so the glyph tracks the label's size rather than being a separate
   number -- 14px here, matching the `!text-[length:var(--font-size-sm)]` the
   Codex capture shows on its own button icons. The kit gives its svg
   `height: 1em` and lets the width follow the glyph's aspect ratio, and
   fa-share-nodes is a 448-unit viewBox, so unpinned it would sit narrower than
   it is tall. `flex: none` is Codex's `shrink-0`, and the same guard the section
   glyphs take at :1039. */
.article-share-label > svg {
  width: 1em;
  flex: none;
}

/* HOVER IS CODEX'S OWN RULE, VERIFIED, not a derivation. The badge pass had to
   invent every state here, because Badge is a display component whose frames
   draw no interaction at all. A second capture Joy sent on 2026-10-27 -- the
   Dropdown's trigger with :hover forced -- settled it:

     .kb-button-regular:hover:not([aria-disabled=true],[disabled],[data-loading=true]) {
       background: var(--neutral-bg-component-hover);
       box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
                   var(--shadow-hover);
     }

   which is the rule below declaration for declaration. The `:not()` guard has no
   counterpart here on purpose: a <summary> has no disabled or loading state to
   exclude. That capture also showed `.kb-button`'s resting box-shadow struck
   through while hover was forced, which is the expected shape of the override
   and not evidence the resting shadow is dead.

   What is still this file's decision is only the [open] state, since a Codex
   button has no menu to be open. It reuses the press fill: a trigger that stays
   looking held-down for as long as its menu is down is the honest reading, and
   it costs no invented fourth step.

   The border does not move in any state. Codex's is --neutral-border-alpha-strong,
   an alpha of the ink, so it already darkens against each successive fill on its
   own -- moving it as well would double the effect.

   One thing NOT carried over from the badge pass: there is no separate `color`
   on hover and no second rule for the glyph. The icon inherits now, so the label
   and the glyph cannot drift apart -- which was the half-dead-hover problem the
   note at :988 had to work around on the sidebar headings. */
.article-share-label:hover {
  background-color: var(--neutral-bg-component-hover);
  box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
    var(--shadow-hover);
}

/* Press is immediate and release settles back over the base rule's duration,
   which is the convention .btn:active (css-buttons.css:153) sets. The shadow
   drops back to the resting one rather than staying lifted: the control is being
   pushed in, so a bigger shadow would fight the fill. */
.article-share-label:active {
  background-color: var(--neutral-bg-component-active);
  transition-duration: var(--motion-duration-instant);
}

.article-share[open] .article-share-label {
  background-color: var(--neutral-bg-component-active);
}

/* THE FOCUS CUE IS AN INSET RING. This is the per-control cue
   css-reset.css:62-75 promised for the article_page round, and when it was
   written it was the only option: .article-share was `overflow: hidden` with no
   padding and the summary is its flush first child, so the sitewide
   `*:focus-visible` outline painted outside a box that was being clipped. An
   inset ring paints inside the border box, so it survived.

   THE CLIP IS GONE NOW (:1437 records why), so the plain outline would work here
   too. The ring is kept anyway, for two reasons rather than inertia: it is what
   the reset's own note points at for this control, and an inset shadow follows
   `border-radius` exactly, so it hugs the pill where an outline at
   max(2px, 0.15em) offset draws a visibly rounder shape around it. Reverting is
   deleting both rules below; nothing else depends on them.

   `outline: none` is paired with it on purpose -- without it this control would
   draw both cues at once, which is the doubling the reset's note warns about.

   THIS ALSO FIXES THE WHITE-ON-WHITE BUG Joy deferred earlier in October. The
   old cue was `.article-share-label-wrapper:focus-within { background-color:
   var(--clr-neutral-160) }` plus `:focus-within * { color: var(--clr-white) }`
   -- a universal descendant selector, so it painted the label, the glyph and
   anything the share menu held white, against a fill that only covered the
   summary. :focus-visible rather than :focus-within is the other half: the
   wrapper matched :focus-within whenever anything inside the OPEN menu held
   focus, so tabbing to Facebook restyled the trigger behind it.

   THE RESTING DROP SHADOW IS RESTATED ALONGSIDE THE RING, and it has to be:
   `box-shadow` is a single property, so a rule declaring only the inset ring
   replaces the drop shadow instead of adding to it, and the control would go
   flat for exactly as long as it held focus. Inset first so the ring paints over
   the fill rather than under it.

   This also means a focused control does not take the hover lift -- this rule is
   (0,3,0) against :hover's (0,2,0), so it wins while both match. That is the
   right way round: a focus ring is information and a lift is decoration. */
.article-share-label-wrapper:focus-visible {
  outline: none;
}

.article-share-label-wrapper:focus-visible .article-share-label {
  box-shadow:
    inset 0 0 0 2px var(--form-focus-ring),
    0 var(--shadow-y-offset) var(--shadow-blur) 0 var(--shadow-regular);
}

/* THE TRIGGER'S HIT AREA NOW MATCHES WHAT IT DRAWS. .article-share is a flex
   column whose width is set by its widest child, so while the menu is open that
   is the 12rem .share-dropdown -- and the summary, a stretched flex item, was
   12rem wide with a content-width control inside it. Clicking empty space to the
   right of the visible box toggled the menu. Invisible when closed, because the
   dropdown is `display: none` and the trigger is then the widest child. Button
   padding narrows the dead strip (the box is ~9.5rem now, not ~3.5rem) but does
   not close it, so this still has to be declared. */
.article-share-label-wrapper {
  align-self: flex-start;
}

/* The share menu, rebuilt 2026-10-22 as the third of the theme's three
   dropdowns. The other two are the header's language and user popovers, and they
   are built from `.dropdown` in css-buttons.css -- see the long note on that
   section for where these values come from and how they map onto the search
   autocomplete panel, which is the reference for all of them. This panel repeats
   the declarations rather than sharing them because it shares no class name with
   those two: it is a <details> body emitted around Zendesk's `{{share}}` helper,
   not a `.dropdown`.

   Everything that made this a differently-shaped card has gone: the
   --clr-neutral-40 border, the --clr-white fill, the 0.25rem radius, the
   hand-rolled `0px 4px 6px rgba(0,0,0,0.1)` shadow (none of which had a dark
   tier), the 0.5rem gaps at both levels, and `padding: 0.5rem 0`.

   Three behaviours came off as well, and they are the reason this was worth
   touching rather than just recolouring:

   - `.share-dropdown:hover` darkened the *card's* border whenever the pointer was
     anywhere inside it. The rows now answer the pointer; the card does not.
   - a bare `svg:hover` painted a fill behind the icon alone, so hovering the
     glyph and hovering its label did two different things in the same row.
   - `li:hover *` set a background on every descendant of the row. A background
     on each inline child draws one rectangle per box, which on a row of
     icon + label is a fill with a seam through it -- the same ragged-rectangle
     failure the reset's `a:not([class]):hover` causes on wrapping links. The fill
     belongs on the row's anchor, which is one box.

   What stayed: `inline-size: 12rem`, which is this panel's own geometry and not a
   dropdown-wide value, and the nth-child rule hiding the second glyph in the X
   row, which is a workaround for what `{{share}}` emits. */

/* THE CARD WAS ALREADY RIGHT, verified 2026-10-27 against Codex's own Dropdown
   panel, which Joy opened in DevTools when she asked for "this exact style".
   Codex builds that panel out of Tailwind utilities rather than a .kb-* rule, so
   the element carries its whole spec in its class attribute:

     slide-up-and-fade  z-floating  max-h-60  overflow-auto  min-w-43.75
     max-w-(--reka-dropdown-menu-content-available-width)
     rounded-sm  border  border-neutral-soft  bg-neutral  p-1
     shadow-floating  will-change-[opacity,transform]  outline-none

   Four of those were already declared here, and the match is exact rather than
   near: `rounded-sm` is --radius-sm; `border border-neutral-soft` is one
   --border-width-regular of --neutral-border-soft; `bg-neutral` resolves to
   --background-color-neutral, which is Codex's Tailwind-namespaced name for what
   we hold as --neutral-bg, the same renaming css-search.css:191 already works
   through for --text-color-neutral; and `p-1` is --spacing * 1, so 0.25rem.
   `shadow-floating` expands to the four --shadow-floating-* primitives this rule
   assembles by hand, in the same order -- css-brand-tokens.css:536 is the note on
   why that composite is the one Codex shadow we are allowed to name.

   I READ THE SCREENSHOT WRONG BEFORE THE CAPTURE ARRIVED, which is worth leaving
   on the record because it is the argument for the capture. In Joy's first
   Dropdown image the card looked borderless and rounder than 6px, and I was
   about to spend a change on both. It is neither: a floating shadow sitting over
   a soft border at a 6px radius is simply not measurable by eye.

   TWO THINGS ARE GENUINELY NEW, and neither shows at default scale:

   - `max-h-60` and `overflow-auto`, written here as max-block-size: 15rem and
     overflow: auto. The share helper emits three rows and print makes a fourth,
     so at ~35.6px a row the card stands around 150px and never reaches the cap.
     It earns its keep at large text zoom and in languages whose rows wrap --
     the same case the row's `overflow-wrap: anywhere` is there for.

     THE COST IS A TENTH OF A PIXEL OF FOCUS RING, stated here so nobody has to
     rediscover it. `overflow: auto` makes this a scroll container, and a scroll
     container clips at its padding box whether or not it is scrolling. A row's
     ring reaches 2px of pinned offset plus an outline of max(2px, 0.15em),
     which is 2.1px at the row's 14px -- so 4.1px into 4px of card padding, on
     all four sides. If it ever reads as clipped, the fix is the theme's
     clipped-control idiom rather than more padding: trade the row's
     `--outline-offset` rule for `box-shadow: inset 0 0 0 2px
     var(--form-focus-ring)`, as the theme's other clipped controls do.
     `overflow-clip-margin` cannot help -- it needs `overflow: clip`, which by
     definition cannot scroll.

     AND NO, THIS DOES NOT REOPEN WHAT DELETING .article-share's `overflow:
     hidden` FIXED, three screens up at the top of this section. An element's
     overflow clips its descendants, never its own box-shadow, and the shadow at
     stake there was this card's -- clipped because the clip lived on the
     *parent*. Putting a clip on the card itself leaves that shadow untouched.

   - the entrance animation, which is below the rule with its keyframes.

   NOT ADOPTED, deliberately: `min-w-43.75` with the max-width bound to the
   popper's available width. That pair lets Codex's panel size to its content
   between a 175px floor and the viewport. This card keeps `inline-size: 12rem`
   -- 192px, which clears Codex's floor rather than contradicting it -- because
   the rows' `overflow-wrap: anywhere` is tuned for a fixed 12rem, and because
   nothing here is positioned by a popper, so there is no available width to bind
   the other end to. `z-floating` is skipped for a similar reason: it is
   --z-index-floating, 300, but what stacks this panel is .article-share's own
   z-index, which is a separate open question about this sidebar. */
.share-dropdown {
  display: flex;
  flex-direction: column;
  inline-size: 12rem;
  max-block-size: 15rem;
  overflow: auto;

  /* NO GAP, which is Codex's and is the third change off the 2026-10-27 capture
     -- the one Joy asked for by eye before anything was measured ("nested snug
     underneath the button"). Measured on her screenshot of the live Dropdown at
     2x: the trigger occupies y 50-129 (80px, so exactly the 40px control) and
     the panel's top border begins at y 130. Zero. Their left borders are both at
     x 75-76 as well, which is `data-align="start"` -- flush with the trigger's
     start edge, which this flex column already gives us for free.

     This was `margin-block-start: 0.5rem`, and the 8px was not a guess: it is
     what the header's two popovers and the search autocomplete all use
     (css-buttons.css:458, css-search.css:103), so matching it was the right
     instinct and is still right for those two.

     WHY THE SAME 8px IS WRONG HERE, because this is the part worth keeping:
     Joy sent four shots of the same measurement reading three different ways,
     and the variable is the shape of the edge above it, not the number. Under
     the search field the trigger's bottom edge is straight and runs the full
     width of the panel, so 8px reads as 8px -- she called that one basically
     perfect. Under a pill the bottom edge is only tangent across the flat
     middle; toward the corners the silhouette curves up and away, and this
     panel is wider than the trigger besides, so along most of the panel's top
     edge the real distance is more than 8px and the eye takes the largest of
     them. The header's "Requests" menu reads worst of the three for a related
     reason -- its trigger draws no box at all, so the 8px is measured from text.

     What makes it read as nested rather than merely adjacent is the card's own
     --shadow-floating casting *upward* onto the trigger: 20px of blur and 2px
     of spread against only 2px of y-offset, so about 10px of it falls above the
     card. The capture shows exactly that happening on Codex's pill, its fill
     darkening 217 -> 212 -> 207 -> 204 over the last five pixels. We get it
     free, and only at zero gap -- at 8px the shadow landed on background.

     Nothing overlaps, so no paint-order question arises: the panel's border
     starts on the first pixel after the trigger's. It would matter at a
     negative margin, where this card paints over the summary by source order. */
  margin-block-start: 0;
  padding: 0.25rem;
  background-color: var(--neutral-bg);
  color: var(--neutral-text);
  border: var(--border-width-regular) solid var(--neutral-border-soft);
  border-radius: var(--radius-sm);
  box-shadow: 0 var(--shadow-floating-y-offset) var(--shadow-floating-blur)
    var(--shadow-floating-spread) var(--shadow-floating-color);
  font-family: var(--brand-font-family-sans);
  font-size: var(--step-0);
  text-align: start;

  ul {
    display: flex;
    flex-direction: column;
  }

  /* No `width: 100%` on either of these any more. `align-items: flex-start` is
     gone from the panel, so the default `stretch` already makes each row as wide
     as the card -- which is what a row's hover fill needs in order to span it. */
  li {
    display: flex;
  }

  /* One row, and as of 2026-10-27 it is Codex's .kb-dropdown-menu-item, which
     Joy pulled from DevTools when she asked for "this exact style" from the
     Dropdown component. Theirs reads:

       .kb-dropdown-menu-item {
         align-items: center;  border-radius: var(--radius-sm);
         color: var(--text-color-neutral);  cursor: pointer;  display: flex;
         font-family: var(--font-family-secondary);
         font-size: var(--font-size-sm);
         font-weight: var(--font-weight-regular);
         gap: calc(var(--spacing) * 2);        -- 0.5rem
         line-height: 1.4;
         min-height: calc(1lh + var(--spacing) * 4);
         overflow-wrap: anywhere;
         padding-block: calc(var(--spacing) * 2);    -- 0.5rem
         padding-inline: calc(var(--spacing) * 2.5); -- 0.625rem
       }

     MOST OF IT WAS ALREADY SATISFIED, because this row was built from
     .kb-option-item -- the Dropdown's sibling component -- and the two are very
     nearly the same rule. The equivalences, all of which css-search.css:191
     already works through for the autocomplete: --font-family-secondary *is*
     Inter and so is --brand-font-family-sans on the panel; --font-size-sm is
     14px, which is exactly what the panel's --step-0 clamps to at its minimum;
     --text-color-neutral is Codex's name for what we hold as --neutral-text,
     also on the panel; --font-weight-regular is 400, the body default nothing
     here overrides; and 1.4 is --leading-body-0. All decided once, one level up.

     THREE THINGS GENUINELY CHANGED:
       - `min-height`, which is new. 1lh + 1rem, so a row is at least one line
         plus its padding however the content wraps -- and `lh` is the right unit
         rather than a hardcoded 35.6px, because it tracks the line-height above
         it. First use of `lh` in the theme; Chrome 109 / Safari 16.4 / Firefox
         120, so it is safe here, and a row would merely be shorter if it were not.
       - `overflow-wrap: anywhere`, which matters more than it looks: the share
         rows are localised `dc` strings in a 12rem card, so a long compound in
         German or Finnish would otherwise push the panel wider.
       - the inline padding, 0.75rem -> 0.625rem. THIS IS THE ONE PLACE WHERE
         "EXACTLY CODEX'S DROPDOWN" AND "MATCH THE SIGN-IN MENU" DISAGREE, and it
         is worth 2px of attention: .kb-dropdown-menu-item uses --spacing * 2.5
         where .kb-option-item-flush uses * 4, and our autocomplete and header
         rows sit at 0.75rem between the two. Joy asked for the Dropdown
         component by name, so the Dropdown's value wins here. Reverting to
         0.75rem is a one-line change if the 2px reads worse beside the header
         menu than the inconsistency does.

     The inline padding was 1.5rem before any of this, an indent deep enough that
     the icons sat a third of the way into the card. */
  li > a {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1;
    min-height: calc(1lh + 1rem);
    border-radius: var(--radius-sm);
    color: var(--neutral-text);
    cursor: pointer;
    line-height: var(--leading-body-0);
    overflow-wrap: anywhere;
    padding-block: 0.5rem;
    padding-inline: 0.625rem;
    text-decoration: none;
    transition: background-color var(--motion-duration-control)
      var(--ease-in-out);
  }

  li > a:hover,
  li > a:focus-visible {
    background-color: var(--neutral-bg-component-soft-hover);
  }

  li > a:focus-visible {
    /* `border: 0` dropped 2026-10-25: the reset no longer pairs a border with
       its outline, so there is nothing here to cancel. See css-reset.css. The
       offset stays -- Codex uses a flat 2px where the reset falls back to
       max(2px, 0.15em). */
    --outline-offset: 2px;
  }

  li > a:active {
    background-color: var(--neutral-bg-component-active);
    transition-duration: var(--motion-duration-instant);
  }

  /* No colour here: the glyph inherits from the anchor, so it tracks the label
     through every state instead of being painted separately. */
  li > a > svg {
    block-size: 1rem;
    inline-size: 1rem;
    flex: none;
  }

  li:nth-child(2) {
    svg:nth-child(2) {
      display: none;
    }
  }
}

/* The panel's entrance, Codex's own. Off the same capture:
   `.slide-up-and-fade { animation: slide-up-and-fade 0.4s
   cubic-bezier(0.16, 1, 0.3, 1) }`, and the element's own
   `will-change: opacity, transform` names the two properties it moves.

   BEHIND @media (prefers-reduced-motion: no-preference) RATHER THAN ON A TOKEN,
   which is the only judgement call in this block. Reduced motion is usually free
   in this theme because the four --motion-duration-* tokens collapse to zero
   (css-brand-tokens.css:1226), but 0.4s is not one of them -- it falls between
   --motion-duration-moderate at 250ms and --motion-duration-slow at 600ms -- and
   cubic-bezier(0.16, 1, 0.3, 1) is none of our three eases either. Keeping
   Codex's real numbers and gating the whole animation is what the theme already
   does in that situation: css-reset.css:551, css-page-footer.css:385 and
   css-components.css:2528 are all this shape.

   THE 2px IS THE ONE NUMBER THE CAPTURE DOES NOT CONTAIN. Keyframes live in
   Codex's bundle, not in the Styles pane. This is Radix's documented
   slideUpAndFade, and three things say Codex took that recipe verbatim: the
   name, the 0.4s and the curve all match it exactly, and the panel is Reka UI,
   which is the Vue port of Radix. If the real distance turns out to be something
   else, it is this one value.

   `translate` rather than `transform`, and a from-only keyframe, to match
   kb-category-sublist-in in css-page-search-results.css. Nothing animates on
   close: the panel goes `display: none` the instant [open] is gone, and a
   <details> leaves us no closing state to animate out of. */
@keyframes kb-slide-up-and-fade {
  from {
    opacity: 0;
    translate: 0 0.125rem;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .share-dropdown {
    animation: kb-slide-up-and-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  }
}

.article-share[open] .share-dropdown {
  display: flex;
}

.article-share:not([open]) .share-dropdown {
  display: none;
}

.share-print {
  width: 100%;
}

/* Print is a fourth row, but it sits outside the <ul> that {{share}} emits, so
   it cannot be picked up by the `li > a` rules above and has to restate them.
   Any change to a row belongs in both places. */
.share-print a {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: calc(1lh + 1rem);
  border-radius: var(--radius-sm);
  color: var(--neutral-text);
  cursor: pointer;
  line-height: var(--leading-body-0);
  overflow-wrap: anywhere;
  padding-block: 0.5rem;
  padding-inline: 0.625rem;
  text-decoration: none;
  transition: background-color var(--motion-duration-control) var(--ease-in-out);

  &:hover,
  &:focus-visible {
    background-color: var(--neutral-bg-component-soft-hover);
  }

  &:focus-visible {
    /* As on the `li > a` rows above, and for the same reason. */
    --outline-offset: 2px;
  }

  &:active {
    background-color: var(--neutral-bg-component-active);
    transition-duration: var(--motion-duration-instant);
  }
}

/* These three ::afters are the rows' visible labels -- {{share}} emits the
   anchors with no text of their own. No `color` declaration: it was
   `var(--clr-secondary-600)`, a token that is not defined anywhere in the theme,
   so the declaration has been invalid at computed-value time all along and the
   labels have always inherited from the row. Now they inherit on purpose, which
   is what makes them follow the row's hover and press states. */
.share-facebook::after,
.share-twitter::after,
.share-linkedin::after {
  font-size: var(--step-0);
}

.share-facebook::after {
  content: 'Facebook';
}

.share-twitter::after {
  content: 'X';
}

.share-twitter::before {
  font: var(--fa-font-brands);
  content: '\e61b';
  display: none;
}

.share-linkedin::after {
  content: 'LinkedIn';
}

/*****************************/

/* Article Vote */

.article-vote {
  display: flex;
  margin-block-start: 3rem;
  width: 100%;
  justify-content: flex-start;
  gap: 1rem;
  align-items: center;

  /* THE PROMPT'S INK. This said "for both vote blocks" until 2026-10-29, when
     Joy had the top one removed -- the footer widget is the only thing left that
     matches, via its `class='article-vote article-vote-footer'`, and that is
     what keeps this rule load-bearing rather than dead. Joy reported it 2026-10-27 from a
     dark-mode capture: "Is this article helpful?" was rendering as dark grey on
     the dark card, all but invisible. --clr-neutral-160 is #333333 in
     css-root.css and has no dark tier, so it was painting #333333 on #2e3e4e.

     --neutral-text rather than --neutral-heading, which is the same value in
     both tiers (#10171e / #ebf0f4) and would have been the other defensible
     pick -- this is a <p>, so the paragraph role is the literal one. Nothing
     about the rendering distinguishes them if that is ever revisited.

     The declaration lives here alone and not on .article-vote-footer's `p` as
     well. Both rules are (0,1,1) and the footer's is later in the file, so it
     used to win with an identical colour; it now carries only the font-size it
     actually changes. The type is still legacy on purpose -- the weight, size
     and line-height are untouched and out of this phase's scope. */
  p {
    color: var(--neutral-text);
    font-weight: var(--font-bold);
    font-size: var(--step-0);
    line-height: 1.5;
  }

  /* CODEX'S BUTTON, REGULAR EMPHASIS -- the same control as the share trigger,
     which is what Joy asked for on 2026-10-27 ("the same button style -- padding,
     border radius, etc. as the share button"). Every value here is copied from
     .article-share-label at :1599 rather than re-derived, so the two stay one
     control with one spec; change it there and change it here.

     WHY THE RESTING TREATMENT IS ON THIS RULE AND NOT ON THE TWO CLASSES, which
     is the only non-obvious thing in this block. Nested, this is
     `.article-vote button` -- (0,1,1), a class and a type -- while
     .article-vote-up and .article-vote-down are (0,1,0). The nested rule wins,
     so putting the padding and radius on the severity classes would have left
     the old 0.25rem radius and 0.25rem/0.5rem padding standing and looked like
     the cascade was broken. It is also the right home on its own terms: both
     buttons rest identically and differ only in state colour. The state rules
     below are (0,2,0) and clear this comfortably.

     `line-height: 1` is load-bearing, same arithmetic as the share trigger:
     0.75rem of padding twice, plus 2px of border, plus a 14px label at a
     line-height of exactly 1, is the 40px control. The 1.5 this rule used to
     inherit from the shared block would have drawn it at 47px.

     The transition gains border-color and color, which the share trigger does
     not need -- its colours hold still across every state and only the fill and
     shadow move. These two hold still on hover as well, as of 2026-10-27; what
     still needs the extra pair is the selected state, where the border and the
     ink both change with the severity fill. */
  button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding-block: 0.75rem;
    padding-inline: 1rem;
    border: var(--border-width-regular) solid var(--neutral-border-alpha-strong);
    border-radius: var(--radius-round);
    background-color: var(--neutral-bg-component);
    color: var(--neutral-text);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
    font-family: var(--font-family-primary), system-ui, sans-serif;
    font-size: var(--font-size-sm);
    line-height: 1;
    transition:
      background-color var(--motion-duration-control) var(--ease-in-out),
      border-color var(--motion-duration-control) var(--ease-in-out),
      color var(--motion-duration-control) var(--ease-in-out),
      box-shadow var(--motion-duration-control) var(--ease-in-out);
  }

  /* THE SHADOW LIFT IS THE ONLY PART OF HOVER BOTH THUMBS SHARE. It is the
     share trigger's, which is Codex's `.kb-button-regular:hover` captured from
     DevTools and quoted at :1685. The fill and the ink that rule also moves are
     on the severity classes instead, since they differ per thumb.

     THE BORDER DOES NOT MOVE ON HOVER, and that half is settled rather than
     experimental. Joy, 2026-10-27: "remove the changing hover borders on both
     the up and down votes." The previous pass gave the upvote a green hover edge
     and the downvote a red one; she caught the red as a destructive-looking edge
     on a button that only records an opinion, and the green went with it. She had
     not seen the green at all -- the upvote in her preview was stuck in its
     selected state, which is also what made the whole hover spec look wrong
     earlier in the day. Codex holds the border still in every state anyway
     (:1702): --neutral-border-alpha-strong is an alpha of the ink, so it already
     darkens against each successive fill on its own.

     This also retires the "contrast goes down on hover" note this block used to
     carry. It was real -- hover lightened the fill to --neutral-bg and softened
     the ink to --neutral-text-soft -- and it was worse than that note said,
     because --neutral-bg became .article-container's background the same day, so
     the hover fill matched the card exactly and the control dissolved into it. */
  button:hover {
    box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
      var(--shadow-hover);
  }

  /* The thumb, injected by vote-icons.js and swapped to an <svg> by the kit.
     1em rather than a rem so it tracks the label, and no colour of its own --
     the kit's svg carries `fill: currentColor`, so it follows the button's ink
     through all four states and cannot drift away from the word beside it. */
  button > svg {
    width: 1em;
    flex: none;
  }
}

/* THE DARK FILL PERSISTS NOW, which is all this pass is about. Joy reported it
   2026-10-27: clicking either thumb turns it dark, and clicking anywhere else
   turns it straight back, in both the top block and the footer.

   `:focus-within` was doing it. The only rule that ever painted the dark fill
   was a focus rule, so the colour was tracking *focus* rather than the vote, and
   clicking elsewhere takes focus with it. There was no selected state in this
   file at all -- nothing was broken, it had simply never been written.

   WHAT SUPPLIES THE STATE INSTEAD: Zendesk's own `selected_class` parameter on
   the vote helper, now set on all four buttons in article_page.hbs. The markup
   Joy captured carried `data-selected-class="null"`, so the helper was already
   reading the parameter and finding nothing in it. Worth preferring over
   `aria-pressed`, which was the other candidate: the script applies the class
   itself, so we do not have to be right about which attribute it flips or when.

   The press fill stays, on `:active`, using the exact colours `:focus-within`
   used. So the instant of clicking looks precisely as it did and only the
   persistence is new. Selected is ordered after `:hover` and both are (0,2,0),
   which is what makes a voted button keep its dark fill when the pointer returns
   to it instead of flickering back to the pale hover tint.

   The focus ring is the reset's now, which is what it should always have been --
   a colour inversion is not a focus indicator. Nothing here sets `outline:
   none`, so `*:focus-visible` in css-reset.css already supplies one.

   WHERE THIS WILL AND WILL NOT SHOW, because Joy found the asymmetry before I
   did: votes only register in the footer block. The top block's buttons do
   nothing server-side, so Zendesk's script has no reason to mark them selected
   and the fill will not stick there. That is the behaviour she is keeping -- the
   footer is the block that survives this phase -- and the cause of the top
   block's silence is hers to chase separately.

   DELETED WITH THE REWRITE, both provably dead rather than merely unused:
     - `&::after { font: var(--fa-font-regular); content: '\f164' }` on each
       button and the `display: none` that hid them. --fa-font-regular is
       undefined in this theme, so the declaration was invalid and the codepoint
       could not have drawn even unhidden; vote-icons.js is the note on why.
       Joy confirmed 2026-10-27 that the injected thumbs survive a click, so
       that script needs no mutation observer after all.
     - `&:focus-within *` on the down button, which set a fill and an ink on
       every descendant. A background on each inline child draws one rectangle
       per box, which is the seam-through-the-fill failure described in the
       share rows' note above. Unnecessary as well as wrong: the kit's <svg>
       carries `fill: currentColor`, so the glyph follows the button's own
       `color` through every state on its own.

   STILL LEGACY, DELIBERATELY. Every colour here is a --clr-* primitive with no
   dark tier, and the box is a 1px/0.25rem control rather than a Codex one.
   Restyling these to the Button component is the next job on this block; this
   change is only the state, so the values are left exactly as they were and the
   diff stays readable. */
/* THE SEVERITY STATES, on Codex's roles as of 2026-10-27: a tint on hover and
   the strong fill once the vote is recorded. Two emphases of one severity, so
   the control gets greener (or redder) the further it is pressed.

   THE HOVER TINT STARTED AS AN EXPERIMENT AND IS SIGNED OFF. Joy, 2026-10-27:
   "can we try something just for fun? When you hover on the upvote button, what
   would it look like if it brought in the success-regular colors, and on the
   downvote if it was the critical-regular colors? Then when selected it would
   take the strong settings." Then, on previewing it: "this is fantastic!"

   Worth knowing if it is ever retuned: in light mode this is a hue shift, not a
   brightness one. --neutral-bg-component (#deebec) to --success-bg (#e0fdcf) is
   pale cyan to pale green at almost the same lightness, 1.09 against the card.
   It reads as colour arriving rather than as the control lighting up, and that
   was checked in preview and wanted. Dark mode moves further (blue-grey to olive
   #364f1d, or to deep maroon #4f0102 on the downvote). The lever if the upvote
   ever needs more presence is --success-bg-strong-hover, not --success-bg-soft,
   which is paler than --success-bg rather than stronger.

   IT IS BUILT ON `soft`, NOT ON `regular`, AND THAT IS A DELIBERATE DEPARTURE
   FROM HER WORDING. The measured badge table at :1507 is the reason -- regular's
   label is --neutral-text-soft for all six severities and its background is
   --neutral-bg, so the only coloured thing about a regular badge is its border,
   and she removed the hover border in the same message. Regular-minus-border is
   a control with no colour in it at all. `soft` is the row that actually tints:

     emphasis   background        border                 label
     soft       {sev}-bg          {sev}-border           {sev}-text
     strong     {sev}-bg-strong   {sev}-border-strong     --fixed-white, or
                                                          --fixed-black on
                                                          warning and success

   The border column is ignored in both rows here, per the hover note above and
   because the resting border is Codex's own choice for this control.

   THE THUMB IS LEFT FOLLOWING THE INK rather than taking {sev}-icon, which is
   what the table's second finding (:1520) would give a badge. The svg carries
   `fill: currentColor` and no colour of its own, so it tracks the label through
   all four states; splitting it off would be a second decision to maintain for a
   state that may not survive preview.

   ORDER IS LOAD-BEARING AGAIN NOW THAT HOVER IS BACK ON THESE CLASSES. `&:hover`
   and `&.article-vote-selected` are both (0,2,0), so the cascade falls to source
   order and hover MUST stay above selected -- otherwise a voted button drops to
   the pale tint whenever the pointer returns to it, which is the flicker the
   previous pass was written to avoid.

   So success strong takes --fixed-black and critical strong takes --fixed-white.
   That asymmetry is not a slip: css-components.css:1665 has it from the other
   direction, where the callouts' dark tier does the same thing, and the reason
   is that the success and warning strong fills are bright enough (#a3e567,
   #fbb930) that white ink on them fails. `--fixed-*` rather than
   `--neutral-text-inverse` because these fills do not flip with the theme, so
   their ink should not either.

   --critical-bg-strong is the one role here with no dark tier, and that is
   deliberate rather than a gap: :850 in css-brand-tokens.css records it as the
   brand red in both modes. An earlier version of this comment claimed every
   role below had a dark tier; it does not, and nothing depends on it having one.

   The instant press is the share trigger's, for the reasons given there. Both
   rules are (0,2,0) so they clear the resting rule's (0,1,1) and the hover
   rule's (0,1,2) regardless of source order -- which is what keeps a voted
   button's strong fill from flickering back to the hover tint when the pointer
   returns to it.

   `border-color`, not the `border` shorthand the legacy rules used: the width
   and style belong to the resting rule now, and restating them here would mean
   three places to change a 1px border instead of one. */
.article-vote-up {
  &:hover {
    background-color: var(--success-bg);
    color: var(--success-text);
  }

  &:active,
  &.article-vote-selected {
    background-color: var(--success-bg-strong);
    border-color: var(--success-border-strong);
    color: var(--fixed-black);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
  }

  &:active {
    transition-duration: var(--motion-duration-instant);
  }
}

.article-vote-down {
  &:hover {
    background-color: var(--critical-bg);
    color: var(--critical-text);
  }

  &:active,
  &.article-vote-selected {
    background-color: var(--critical-bg-strong);
    border-color: var(--critical-border-strong);
    color: var(--fixed-white);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
  }

  &:active {
    transition-duration: var(--motion-duration-instant);
  }
}

/* TOMBSTONE, 2026-10-29: `@media (min-width: 45em) { .article-vote {
   margin-block-start: 0; justify-content: flex-end } }`.

   It existed to pull the TOP vote block over against the share trigger on wide
   viewports, and Joy had that block removed. What is left of .article-vote is
   the footer widget alone, and this could never reach it: .article-vote-footer
   is also (0,1,0) and sits later in this file, so its own `margin-top: 0` and
   `justify-content: center` already beat both of these declarations at every
   width. Deleting it therefore changes nothing that renders -- it was inert for
   the footer before the top block went and is now unreachable entirely.

   Worth knowing if the vote CSS is ever consolidated: with the top block gone,
   .article-vote and .article-vote-footer describe the SAME element, and several
   of the base rule's declarations are in the same position this media query was
   -- `margin-block-start: 3rem`, `justify-content: flex-start` and the `gap`
   are all overridden by the footer rule. Collapsing the two is a real
   simplification and deliberately not done here, because this change was meant
   to remove a section rather than restructure the component. */

/*****************************/

/* Article Navigation */

/* ONE LINK PER SIDE, 2026-10-29. Each .article-pagination used to be a div
   holding two anchors to the same URL -- a bordered chevron box and the article
   title -- and is now a single <a> wrapping the chevron, the eyebrow and the
   title. Joy chose that structure. It halves the tab stops and, more to the
   point, gives the chevron an accessible name, which it had none of while its
   anchor's only content was an aria-hidden glyph.

   THE SHAPE IS THE CHEVRON CENTRED BESIDE THE STACK, Joy's pick from three on
   2026-10-29 after seeing the first pass: the box moves to the outer edge and
   centres against both lines, and the eyebrow and title share one edge. The
   first pass put the chevron in a row WITH the eyebrow, which left the block
   with a ragged side -- the eyebrow indented past the box, the title flush with
   it -- and made the box look like it belonged to the label rather than to the
   pair. Codex has no prev/next component to copy, so this is ours.

   EVERY COLOUR IN HERE WAS A LEGACY --clr-* PRIMITIVE, so dark mode was broken
   outright: --clr-neutral-160, --clr-neutral-5, --clr-white and
   --clr-secondary-140 are each declared once in css-root.css with no dark tier,
   and this component painted #333333 ink with an #f5f5f5 hover straight onto
   the dark card. The replacements, measured against the card's --neutral-bg in
   both tiers:

     eyebrow          --neutral-text-soft         6.61 /  8.09:1  AA / AAA
     title at rest    --neutral-text-soft         6.61 /  8.09:1  AA / AAA
     title on hover   --brand-text                8.12 /  8.58:1  AAA both
     chevron          --neutral-text-soft         6.61 /  8.09:1  AA / AAA
       on hover fill                              5.82 /  6.07:1  AA both
     box border       currentColor, so as above   6.61 /  8.09:1  3:1 is its bar

   ALL FOUR SHARE ONE INK by the time Joy had finished tuning it, and that is
   the design rather than an oversight -- see the notes at .arrow-link and
   .arrow-chevron. Weight and the underline carry the hierarchy inside the
   block; the ink carries only the hover.

   THE TITLE'S HOVER IS THE PROSE-LINK IDIOM settled in the links pass --
   --brand-text on hover and active, underlined throughout -- even though its
   rest ink is no longer the inherited body colour that idiom assumes.
   --clr-secondary-140 was a purple that appears nowhere else on the refreshed
   page.

   THE HOVER FILL IS MEASURED AS A STATE CHANGE RATHER THAN AS CONTRAST, because
   a ratio cannot see a step between two surfaces. --neutral-bg-component-soft-hover
   against the card is dE00 4.42 light / 6.65 dark, both well past the ~2 that
   reads as a different colour at all. It is the hover rung of the *soft*
   component ladder, which is the right ladder for a box that rests transparent
   on the card rather than filled.

   FOCUS IS THE SITEWIDE RING AND NOTHING ELSE NOW. The old `&:focus-within`
   filled the chevron box with #333333 and set white ink on it, and the chevron
   vanished every single time: the nested `svg { color: --clr-neutral-160 }`
   declared a colour on the svg itself, and a declared value beats an inherited
   one at any specificity, so the white never reached the glyph and a dark
   chevron sat on a dark fill. :focus-within also fires on a mouse click, which
   is not what a focus treatment is for. Deleting the block hands the job to
   css-reset.css's `*:focus-visible`, which is what the rest of this page does;
   the nav sits in no clipped context, so that outline draws whole with no
   :has() indirection. Per Joy's standing rule the ring does not bring the hover
   fill along with it.

   PAGE FLOW RIDES THE SCALE AND THE CARD'S INSIDES STAY FLAT. The top margin
   and the gap between the two cards separate components, so they move onto
   --space-xl and --space-m, whose ranges bracket the 3.5rem and 2rem they
   replace. The gaps *inside* a card -- chevron to text, eyebrow to title --
   stay flat.

   THE TITLE IS NO LONGER HIDDEN ON MOBILE. `@media (max-width: 45em)
   { .arrow-link { display: none } }` used to drop it, which was survivable
   while the title had its own anchor and merely disappeared. With one link per
   side it would cut the link's accessible name down to the eyebrow and leave a
   screen reader two stops that both say "Previous article". The cards stack
   instead and both read from the start edge, which gives each one a full column
   for a title that wraps. */

.article-navigation {
  margin-block-start: var(--space-xl);
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  gap: var(--space-m);
  width: 100%;
}

/* THE UNDERLINE ON THIS ANCHOR IS THE BROWSER'S, and turning it off is the
   whole reason `text-decoration-line` appears here. css-reset.css's bare `a`
   rule pins --link-underline and text-underline-offset but never declares
   text-decoration-line, so the line itself is the UA default. That was harmless
   while the anchors were the chevron -- no text to underline -- and the title,
   which wanted one; the moment the card became the anchor it underlined the
   eyebrow too, which is how Joy's first preview came back with "Previous
   article" looking like a second link. A descendant cannot cancel an ancestor's
   decoration, only add its own, so it has to come off here and go back on at
   .arrow-link.

   THE LONGHAND, NOT `text-decoration: none`. The shorthand would reset
   text-decoration-thickness and text-underline-offset to their initial values on
   this subtree and cost the title the house underline metrics.

   `align-items: center` is what carries the shape -- the chevron box is a flex
   item next to a two-line column, so centring the cross axis is what lands it
   against the middle of the stack rather than the top of it. The bare `a` rule
   in css-reset.css happens to set the same value, and that is luck rather than
   a mechanism to lean on: it is declared here because this rule owns the
   layout. */
.article-pagination {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 1rem;
  width: 100%;
  color: inherit;
  text-decoration-line: none;
}

/* Both cards are `width: 100%` and shrink from the same basis, so a pair lands
   on equal halves and a lone card takes the whole row. That is what retires
   `.article-pagination:nth-child(2)`, which this replaces: it could only ever
   match the next card when both existed, and the old template reached for
   `flex-end` and `percent-50` utility classes to align a next-only card that
   was :nth-child(1). A modifier on the element is right in all four
   combinations and needs no help from the markup.

   `text-align` is the only declaration it needs. The chevron lands on the outer
   edge from the markup -- the next branch emits the text column first -- and
   .arrow-text's `flex: 1` takes the leftover width, so there is nothing for a
   justify-content to do. */
.article-pagination-next {
  text-align: end;
}

/* `flex: 1` so the column takes the width the chevron leaves, which is what
   gives `text-align: end` something to move the text within; `min-inline-size:
   0` because a flex item's automatic minimum size is its content, and without
   it a long unbreakable title would push the card past its half of the row
   instead of wrapping.

   THE GAP IS 0.25rem AND NOT THE 1rem IT REPLACED. These two lines are a label
   and the thing it labels, so they read as one block -- the same binding the
   article's headings have to their prose. At 1rem the eyebrow floated between
   the chevron row and the title and belonged to neither. Flat, because it is
   inside a component. */
.arrow-text {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  flex: 1;
  min-inline-size: 0;
}

/* THE EYEBROW WAS LARGER THAN THE TITLE IT INTRODUCES. It carried `.step-2
   fw-600` in the template against the title's then --step-1, so the label
   outsized its own content by a step and a half. --step-0 is the size UI chrome
   opts down to in this theme (see the scale's note in css-root.css), and the
   soft ink is the same pairing the sidebar eyebrows use. The weight is
   unchanged: `fw-600` and --font-bold are both 600.

   It ended up the heavier of the two once Joy brought the title down to meet it
   -- same size, same ink, 600 against 400. That inversion is intentional: the
   eyebrow states what the link does and the title is the detail. Do not read
   this rule as the old defect reappearing from the other direction. */
.arrow-label {
  font-size: var(--step-0);
  font-weight: var(--font-bold);
  color: var(--neutral-text-soft);
}

/* THE TITLE IS QUIETER THAN ITS OWN EYEBROW, which is Joy's call on 2026-10-29
   after previewing the centred shape: "decrease the font size and font weight
   on the article name, and give it the same color as Previous article". It came
   down from --step-1 bold --neutral-text, which made it the loudest thing at
   the foot of the article. The eyebrow and the title now share a size and an
   ink and the weight is the only thing separating them, so the pair reads as
   one quiet block rather than a heading with a label stuck above it.

   It keeps the underline and the --brand-text hover anyway. Those are the two
   things still marking it as the link's subject, and with the ink no longer
   distinguishing it from the eyebrow they are doing more work than before.
   --neutral-text-soft measures 6.61:1 light / 8.09:1 dark on the card.

   The underline is re-declared here because .article-pagination turned the
   anchor's off. Thickness has to be restated with it: text-decoration-thickness
   is not an inherited property and the line is painted by whichever element
   declares it, so the value set on the bare `a` rule does not reach this span.
   --link-underline does reach it -- custom properties inherit -- and so does
   text-underline-offset, which is why only the one longhand is repeated. */
.arrow-link {
  font-size: var(--step-0);
  font-weight: var(--font-regular);
  color: var(--neutral-text-soft);
  text-decoration-line: underline;
  text-decoration-thickness: var(--link-underline);
}

.article-pagination:hover .arrow-link,
.article-pagination:active .arrow-link {
  color: var(--brand-text);
}

/* A SPAN AND NOT THE <i>, because the Font Awesome kit replaces the <i> with an
   <svg> at runtime -- the border and the fill need an element the kit will not
   throw away. Two declarations here are new rather than retokenised:
   `align-items: center`, because the old box only centred horizontally and left
   the glyph sitting on the box's text baseline, and `flex-shrink: 0`, because
   the box must hold its square while the text column beside it wraps. The
   radius moves 0.25rem -> --radius-sm, which is 0.375rem; --radius-sm is the
   smallest radius token this theme has, so this is 2px rounder than before and
   the nearest honest token.

   THE GLYPH IS ON A TEXT TOKEN AND NOT AN ICON ONE, and that is a deliberate
   deviation rather than a slip -- recorded in plan.md's Phase 1 "Documented
   deviations". Joy asked for the arrow to take the eyebrow's colour exactly,
   and no --neutral-icon-* role carries that value: --neutral-icon is #384649,
   --neutral-icon-soft is #96a6a9, and the eyebrow is #4f5f63. Naming the icon
   role would have been value-wrong, which is the worse of the two errors. The
   divergence is light-tier only -- --neutral-text-soft and --neutral-icon are
   both #b2c3d2 in the dark tier, so dark mode gets the semantically correct
   value for free. 6.61:1 on the card and 5.82:1 on the hover fill in light,
   8.09:1 and 6.07:1 in dark.

   `currentColor` ON THE BORDER, not a second reference to the same token. Joy
   asked for the component to be uniform -- "let's make it all uniform" --
   after the border was left at --neutral-border-strong (#384649) and ended up
   DARKER than the glyph it enclosed, 9.72:1 against 6.61:1, so the box
   out-weighed its own contents. Keying it to the element's own ink makes the
   uniformity structural: there is one colour declaration in this rule and the
   line cannot drift from the shape again. It also keeps a text token from
   being named a third time for a third job. No --neutral-border-* role sits at
   this value either, and a border is non-text, so 3:1 is its bar rather than
   4.5:1. */
.arrow-chevron {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-shrink: 0;
  inline-size: 1rem;
  block-size: 1rem;
  padding: 0.5rem;
  box-sizing: content-box;
  border: var(--border-width-regular) solid currentColor;
  border-radius: var(--radius-sm);
  color: var(--neutral-text-soft);
}

.article-pagination:hover .arrow-chevron {
  background-color: var(--neutral-bg-component-soft-hover);
}

@media (max-width: 45em) {
  .article-navigation {
    flex-direction: column;
  }

  /* The end alignment is a two-column affordance. Stacked, each card owns a
     full row, so both read from the start edge; the next card's chevron still
     sits on the outer edge because .arrow-text's `flex: 1` holds it there, and
     that is enough to keep the direction readable without throwing the text
     across the row from its own arrow. */
  .article-pagination-next {
    text-align: start;
  }
}

/*****************************/
/* Article Footer */

/* THE SAME CARD AS .article-container (:1399), which is what Joy asked for on
   2026-10-27: "the footer wrapper needs the same container treatment as the
   article body -- i.e. border color, font color, background color."

   IT IS A SIBLING OF THE ARTICLE CARD, NOT A CHILD, and that is what makes the
   identical treatment right rather than a card inside a card. Both sit at depth
   5 inside .article-wrapper (article_page.hbs:235 and :381), so this box paints
   onto the page wash exactly as the article card does, and the two read as two
   surfaces of one kind. Same radius for the same reason -- a nested panel would
   have wanted --radius-sm, a sibling wants the parent value.

   WHAT CAME OFF: a hardcoded `1px solid var(--clr-neutral-40)` and a literal
   0.5rem radius. --clr-neutral-40 is #c2c2c2 in css-root.css with no dark tier,
   so this was a light grey hairline on a #10171e page -- the same defect the
   article card had before today and the same fix.

   `flex: 1 1 20%` is left alone but is almost certainly inert: .article-wrapper
   is a column flex container with content-driven height, so there is no free
   space for `flex-grow` to claim and the 20% basis resolves against an
   indefinite height. Not deleted, because proving it dead is a separate job from
   recolouring it.

   THE TWO CARDS ARE THE SAME WIDTH AND CANNOT DRIFT, so there is no cap here
   and none is needed. This block claimed until 2026-10-27 that the footer ran
   up to 24px wider than the article card, because that card capped at
   `max-width: 70em`. The claim was wrong and Joy is the reason it got checked:
   she reported never seeing the two hold different widths responsively. Both
   boxes are block children of the roughly 855px .article-wrapper, which is as
   wide as that column ever gets, so the 1120px cap was unreachable at every
   viewport and has been deleted (see the note at .article-container). */
.article-footer {
  padding: var(--space-m);
  border: var(--border-width-regular) solid var(--neutral-border-soft);
  border-radius: var(--radius-md);
  background-color: var(--neutral-bg);
  color: var(--neutral-text);
  box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
    var(--shadow-regular);
  flex: 1 1 20%;
}

.article-vote-footer {
  justify-content: center;
  gap: 1.5rem;
  flex-direction: column;
  margin-top: 0;

  /* Size only. The ink was a duplicate of .article-vote's own `p` rule and both
     are (0,1,1), so this one won on source order alone while declaring the same
     colour; it is on a semantic role in that one place now (:2172). */
  p {
    font-size: var(--step-3);
  }
  button {
    padding: 0.75rem 2rem;
    font-size: var(--step-2);
  }
}

.vote-buttons {
  display: flex;
  flex-direction: row;
  gap: 1rem;
}

/* The vote tally the `vote 'label'` helper emits, which sits under the footer's
   buttons. Same defect as the prompt above and found alongside it: --clr-neutral-100
   is #666666 with no dark tier. --neutral-text-soft rather than --neutral-text,
   because this is a secondary annotation and #666666 was already lighter than the
   prompt's ink -- the role keeps that relationship in both tiers instead of
   flattening the two to one weight. */
.article-vote-label {
  font-size: var(--step-0);
  color: var(--neutral-text-soft);
}

@media print {
  @page {
    size: A4;
  }
  header,
  nav,
  .article-meta,
  aside,
  .back-to-top-wrapper,
  .cta,
  .article-footer,
  .article-navigation,
  .hero,
  footer {
    display: none !important;
  }

  .article-wrapper {
    width: 100%;
    min-width: 100vw;
  }

  /* The fill and the lift go with the border. This rule already dropped the
     border for paper; the card treatment added on 2026-10-27 gave the box two
     more things to strip, and both only show when the reader has "print
     backgrounds" switched on -- a tinted block and a drop shadow on A4. */
  .article-container {
    min-width: 100%;
    border: none;
    background-color: transparent;
    box-shadow: none;
  }

  article {
    widows: 3;
  }

  table,
  img,
  svg {
    break-inside: avoid;
  }
  table td {
    word-break: break-all;
  }

  /* The table scrollers are unwound for paper too, but NOT from this block --
     see the second `@media print` after `.article-content` closes. They have to
     be overridden from inside `.article-content` to outweigh the rules that set
     them, and that is the whole reason this file now has two print blocks. */
}

/* The chevrons mirror for right-to-left, unchanged in behaviour. The selector
   had to move: `.arrow-wrapper > a > svg` described the structure where each
   chevron had an anchor of its own, and after the one-link rewrite there is no
   `a` between the wrapper and the glyph. Keyed on .arrow-chevron, which is the
   element the Font Awesome kit leaves alone. */
[dir='rtl'] .arrow-chevron svg {
  transform: rotate(180deg);
}

/*******************************/

/*************************/
/* Article Title */

.article-header {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-items: flex-start;
  position: relative;
  z-index: 0;
  margin-block: 1.5rem 2.5rem;
  padding: 0;
  border-bottom: 0;
}

/***********************/

/* Article Content */

/* THE ARTICLE'S VERTICAL RHYTHM. IT IS A SYSTEM AS OF 2026-10-28 AND IT USED TO
   BE A SINGLE NUMBER -- one flat 1.5rem between every pair of elements, so a
   heading sat exactly as far from the text it introduced as two unrelated
   paragraphs sat from each other. Joy opened the pass to fix that: "I'd like to
   update the spacing between elements of our article page altogether."

   The relationships, each on the fluid scale at css-root.css:201 so the article
   body finally breathes with the viewport the way the rest of the refresh does
   (320px -> 1440px):

     between flow siblings   --space-s     16 -> 24px
     before h2               --space-l     32 -> 48px
     after h2 / h3           --space-2xs    8 -> 12px
     before h3               --space-m     24 -> 36px
     before h4,h5,h6         --space-s     16 -> 24px
     after h4,h5,h6          --space-3xs    4 ->  6px

   Desktop body rhythm lands on the 24px it has always been; mobile tightens.
   The headings are the visible change: a big gap before, a small gap after, so
   a section binds to its own heading instead of floating between two equal gaps.

   THE GAP AFTER A HEADING IS SET ON THE FOLLOWING ELEMENT, NOT ON THE HEADING,
   and that is not a style choice. Adjacent siblings' block margins collapse to
   the LARGER of the two, so a heading asking for 12px underneath it loses to the
   next paragraph's 24px above it and the gap never shrinks. The only way to pull
   the two together is to reduce the paragraph's own margin-block-start -- hence
   the `+ *` rules. Which in turn makes `margin-block-end: 0` on the headings
   load-bearing: it was in this file for months as a no-op, because the gap it
   looked like it controlled was always coming from the next element.

   A HEADING FOLLOWED BY ANOTHER HEADING GETS ITS OWN VALUE, --space-s, and it
   took two preview rounds with Joy on 2026-10-28 to land on it. The point of
   the tight `after` gap is to bind a heading to the PROSE it introduces, and
   `+ *` includes a heading, so the first version gave an h2 with an h3 directly
   beneath it -- a section opening straight onto its first subsection, common in
   these articles -- the 12px meant for body copy, and the two collided. Letting
   the h3 fall back to its own `before` value was the other extreme at 36px:
   "the 36 is too much". So --space-s, 16->24px, which is the body rhythm: two
   stacked headings sit a normal flow gap apart. That is the `:not(...)` on the
   `+ *` rules plus the descending-pair rules below them.

   THE PAIRS ARE WRITTEN OUT BY RANK rather than as `heading + heading` because
   direction matters. h2 -> h3 is descending, a section opening on a subsection,
   and belongs together at 24px. h3 -> h2 is the opposite -- a subsection with no
   content, then a whole new top-level section -- and should take the h2's full
   48px, which it does because no rule below matches it.

   :first-child HAS TO BE RESET NOW. Nothing had a top margin before, because
   `* + *` cannot match a first child; the headings carry their own, so an
   article opening on an h2 would otherwise start 48px down.

   TABLES ARE IN THE SYSTEM TOO. They were the one thing outside it on the first
   pass, because the table rule in the Tables section carried an `!important`
   that outranked the heading-adjacency rules -- so a heading above a table sat
   at the full sibling gap while a heading above a paragraph sat at the tight
   one. Joy's call on 2026-10-28: "let's change the table styles so that it's in
   alignment with our new system". That rule is now deleted and the note where
   it stood records what the `!important` turned out to be doing (nothing) and
   why deleting beat de-importanting. */

.article-content > * + * {
  margin-block: var(--space-s);
}

.article-content > :first-child {
  margin-block-start: 0;
}

.article-content :is(h2, h3, h4, h5, h6) {
  margin-block-end: 0;
}

.article-content h2 {
  margin-block-start: var(--space-l);
}

.article-content h3 {
  margin-block-start: var(--space-m);
}

.article-content :is(h4, h5, h6) {
  margin-block-start: var(--space-s);
}

.article-content :is(h2, h3) + *:not(h2, h3, h4, h5, h6) {
  margin-block-start: var(--space-2xs);
}

.article-content :is(h4, h5, h6) + *:not(h2, h3, h4, h5, h6) {
  margin-block-start: var(--space-3xs);
}

.article-content h2 + :is(h3, h4, h5, h6),
.article-content h3 + :is(h4, h5, h6),
.article-content h4 + :is(h5, h6),
.article-content h5 + h6 {
  margin-block-start: var(--space-s);
}

/* HEADINGS ON --neutral-heading, BODY COPY ONE RAMP STEP BACK ON
   --neutral-icon, AAA IN BOTH TIERS FROM ONE DECLARATION. This is the pairing
   Joy asked for on 2026-10-27 -- "the headings are the darker color than the
   text" from the search results page -- at the AAA floor (7:1) she then set
   once the measurement was in front of her.

   Measured on the card (#faffff light, #1e2a35 dark):

     --neutral-heading    #10171e   17.89:1     #ebf0f4   12.73:1
     --neutral-icon       #384649    9.72:1     #b2c3d2    8.09:1
     --neutral-text-soft  #4f5f63    6.61:1     #b2c3d2    8.09:1

   WHY NOT --neutral-text-soft, WHICH IS THE OBVIOUS CHOICE AND WAS THE FIRST
   ANSWER HERE: it misses AAA in light at 6.61:1. It shipped for part of
   2026-10-27 as a dark-only override for exactly that reason, which left the
   tiers asymmetrical -- split in dark, flat in light. --neutral-icon makes the
   override unnecessary because its dark value IS the #b2c3d2 that rule was
   setting, so one property now does what two rules did and light gets the
   split too.

   HOW THIS STEP WAS FOUND, because it is not where you would look. Joy pulled
   Codex's own `.prose` block off their site, and the useful thing in it is that
   Codex does NOT dress running text with its semantic text tokens -- that block
   is Tailwind Typography's stock palette, body on gray-700 #364153, headings on
   gray-900 #101828. Its body ink sits at roughly L 37-40%, between our
   --neutral-text and --neutral-text-soft, and it measures 10.21:1 light /
   9.92:1 dark on our card. So the lightness was the finding, and Codex already
   ships a token at it: #384649 is within half a ratio point of #364153.

   THE TAILWIND VALUES THEMSELVES ARE DELIBERATELY NOT USED. Every hex in that
   block is Tailwind's default gray ramp and not one of the eight appears
   anywhere in our Codex token dump; the hues are blue-grey (256-265) against
   our teal light neutrals (--neutral-bg 197, --neutral-border-soft 207).
   `--tw-prose-kbd-shadows: NaN NaN NaN` is the tell that it is the unconfigured
   plugin rather than a Codex decision, and `--tw-prose-invert-headings: #fff`
   is the pure white on dark that Codex's shipped ink deliberately avoids.
   Taking them would have put a second neutral ramp in a theme mid-way through
   a naming pass. Joy's call, 2026-10-27: structure from the find, values from
   Codex.

   THE WART, STATED SO NOBODY HAS TO REDISCOVER IT: --neutral-icon is the icon
   role, used here for text. The value is right and there is no better-named
   token at this step -- --neutral-border-strong is the same #384649, and the
   four text roles are text, text-soft, text-inverse and heading
   (css-brand-tokens.css:465-468), none of which lands here. If Codex ever adds
   a text role at this lightness, this is the one declaration to move.

   --neutral-text-soft KEEPS ITS SECONDARY-TEXT JOB on this page and that is
   unchanged: the tags and date (:1451), the vote prompt and the vote label. It
   is also what the search page uses it for -- the result snippet,
   `#algolia-stats`, the facet links, the pagination -- never for reading copy.
   So the page now has three ink levels, which is what that page has too. */
.article-content {
  color: var(--neutral-icon);

  /* LOAD-BEARING IN BOTH TIERS NOW, AND NOT DELETABLE AS A NO-OP. The rule
     above takes this whole container to --neutral-icon, so this is the half of
     the split that holds the headings up; without it h1-h6 inherit the body
     ink and the page flattens to one level in light and dark alike. It was a
     light-mode no-op for part of 2026-10-27, when the body ink was a dark-only
     override, and that is no longer true.

     --neutral-heading RATHER THAN --neutral-text, which is the same
     #10171e / #ebf0f4 in both tiers, because it states the role -- the point of
     the Codex naming pass Joy opened on 2026-10-27 -- and means headings follow
     Codex automatically if the two ever separate. First use of the token in the
     theme.

     ALL SIX LEVELS, not just the two this file dresses: only h2 and h3 have
     rules below, because those are the two the in-page nav links to, but this
     prose is author-written in the WYSIWYG and an h4 does occur. Grouped with
     `:is()` so it stays one declaration. Its specificity is that of its most
     specific argument -- a type selector -- so this is (0,1,1), level with
     `.article-content h2`, and the h2 block does not restate the colour. */
  :is(h1, h2, h3, h4, h5, h6) {
    color: var(--neutral-heading);
  }

  /* THE THIRD PIECE OF THE SPLIT ABOVE, added 2026-10-28: an unclassed link in
     an article body drops the reset's --hyperlink-link and takes the ink of the
     text around it, which on this container is the --neutral-icon set at the top
     of the rule. 9.72:1 light and 8.09:1 dark on the article card, both AAA;
     --hyperlink-link was 20.00:1 and 12.73:1, i.e. DARKER than the body copy, so
     a link read as the emphasised thing in its own paragraph. Joy's call, and her
     scope: every link in an article body. The classed half is the `color:
     inherit` on `.link` in css-links.css, which carries the reasoning in full.

     IT HAS TO LIVE HERE RATHER THAN IN THE RESET because the reset's rule is
     sitewide and nothing else on the site has --neutral-icon body copy --
     breadcrumbs, request pages and search results all still want the link to
     stand out from what surrounds it.

     `inherit` AGAIN, not the token named outright, for the callout reason given
     in css-links.css: this selector is (0,2,1) and beats
     `[class*='callout-'] a` at (0,1,1), so naming --neutral-icon here would have
     quietly taken callout links off their severity ink before the callout pass
     has decided anything. `inherit` resolves to that same severity ink, so the
     override is a no-op in exactly the place it would otherwise do damage.

     THE HOVER IS RESTATED, not inherited, and that is forced: the reset's
     `a:not([class]):hover` is also (0,2,1), and this file loads after it
     (document_head.hbs:227 vs 211), so the rest declaration above would win the
     tie and a hovered link would never change colour. Nesting puts this at
     (0,3,1), clear of both that and the callout hover.

     AND BEING CLEAR OF THE CALLOUT HOVER IS WHY THE VALUE IS A SLOT, 2026-10-28.
     That phrase was written here when --brand-text was the hover in both places,
     so out-ranking `[class*='callout-'] a:hover` at (0,2,1) cost nothing. It
     stopped being free when callouts stopped moving their ink on hover: at
     (0,3,1) this rule wins inside a callout too -- and loading later
     (document_head.hbs:227 vs 220) means matching specificity would not help
     either -- so a bare <a> in a callout would still have flashed --brand-text
     while a classed one beside it held still. That is the identical
     classed/unclassed split this pass was opened to close, reappearing in the
     hover state after being fixed in the rest state.

     So rather than fight the contest, both rules read the same slot.
     `[class*='callout-'] a:hover` sets --fg-hover to that callout's own --fg, so
     inside a callout this resolves to the REST ink and the colour cannot move no
     matter which selector lands it; in prose the slot is unset and this is
     --brand-text exactly as before. Same trick `inherit` plays above, one state
     along. Why callouts hold their ink at all -- the colour axis there is
     exhausted at both floors in both directions, and the hover is Codex's
     underline weight instead -- is recorded in full on that rule.

     NOT ALSO CHANGED: `.link:hover` in css-links.css, which stays on
     --brand-text outright. It does not need the slot -- it is (0,2,0) and loses
     to the callout's `a:hover` at (0,2,1), which also loads later
     (document_head.hbs:220 vs 217), so a classed link in a callout is already
     decided by the callout. Adding a declaration there that nothing reads is
     how --fg got into trouble in the first place. */
  a:not([class]) {
    color: inherit;

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

  /* THE VISITED STATE, AND IT IS EXTERNAL LINKS IN ARTICLE BODIES ONLY. Joy,
     2026-10-29: "at the minimum I want to apply this to external links clicked
     in articles. For now let's not apply anywhere else."

     THIS IS NOT A REVERSAL of the decision recorded on --hyperlink-link-visited
     in css-brand-tokens.css, which carries the role but leaves it unused
     because honouring it "would have swapped one purple for another on every
     link anybody has already clicked". That objection was about SCOPE -- Joy's
     words were "we're going to keep coming across the purple links everywhere
     we go" -- and this is the narrow case that same note says is one rule away.
     Read the two together, not against each other.

     THE HOOK IS [target='_blank'], the same hook external-link-icon.js uses and
     for the reason its header gives: article bodies are a public API against
     article-authoring-contract.md, and that attribute is the only thing in
     already-published markup that identifies an external link. No authoring
     change and no new class, and the glyph and the colour now describe the same
     set of links. Strictly it means "opens in a new window" rather than
     "external", which is the reading the contract's own .sr-only span uses.

     --hyperlink-link-visited IS CODEX'S OWN TOKEN and its light value is the hex
     Joy sent, so the dark tier comes free. On the article card:
       light  #755fbe on #faffff  5.03:1  AA
       dark   #bdb0f2 on #1e2a35  7.41:1  AAA
     The light tier is the one ink in this refresh that clears AA without
     reaching AAA. It is Codex's published value for the role, so moving it would
     be a deviation from the design system rather than a correction of it --
     recorded here, and in plan.md Phase 1, rather than quietly adjusted.

     And the ratio is not what carries the change: both inks sit on the same
     card, so the comparison is against the REST ink, where the shift is
     dE00 22.9 light and 13.5 dark.

     THE HOVER IS RESTATED, for the reason the a:not([class]) rule above restates
     its own: this selector is (0,3,1) and would otherwise outrank BOTH hovers a
     visited link can take -- `a:not([class]):hover` here, same (0,3,1) but
     earlier in the file, and `.link:hover` in css-links.css at (0,2,0) in a file
     that loads first. A visited external link would have been stuck purple
     through its own hover, classed or not. The restatement at (0,4,1) is clear
     of both. This is also the whole of why :visited is written out longhand
     rather than grouped -- the cascade order LVHA assumes does not survive two
     files.

     BOTH DECLARATIONS READ A SLOT, which is what keeps this rule out of the
     callout pass. Inside a callout, the callout sets --fg to its severity ink,
     so `visited` resolves to that ink and `visited:hover` resolves to it too
     through --fg-hover: the colour does not move in any state, which is the
     invariant callouts were given deliberately and which this must not break
     before the callout pass has decided anything. In prose both slots are unset
     and the fallbacks are the real values. Same trick `inherit` plays for the
     rest state one rule up.

     WHAT THIS CANNOT DO, and it is a browser rule rather than a choice: the
     new-window glyph will NOT follow the ink. A :visited style applies to the
     link element alone and is not inherited by its children -- that is the
     history-leak defence, not an oversight -- so .kb-external-icon's
     `color: inherit` resolves to the UNVISITED colour and the glyph stays body
     ink beside purple text. No selector reaches it: descendants of a visited
     link cannot be styled at all. If that mismatch is unacceptable the answer is
     to drop the state, not to chase the icon.

     The same restriction is why the value is a token reference and worth
     checking once in preview: `color` is on the short list of properties
     :visited may set, and var() resolves off history-independent ancestors, so
     this is sound -- but if a visited external link ever renders in body ink
     instead of purple, the var() is the first thing to rule out. */
  a[target='_blank']:visited {
    color: var(--fg, var(--hyperlink-link-visited));
  }

  a[target='_blank']:visited:hover,
  a[target='_blank']:visited:active {
    color: var(--fg-hover, var(--brand-text));
  }

  /* THE EXTERNAL-LINK CUE, built 2026-10-28, Joy's call on both the shape (the
     box-with-arrow, not a bare diagonal) and the scope (article bodies only --
     prose is where writers link out, and the rest of the theme's new-window
     links are UI). It replaces three `::after` rules in css-links.css that never
     drew anything; see that file for why they could not.

     THE HOOK IS `[target='_blank']` AND IT HAS TO BE. Article bodies are a
     public API -- hand-authored in the WYSIWYG against
     article-authoring-contract.md, which no fix may require anyone to go back
     and edit -- and this attribute is both the only thing in the markup that
     identifies the condition and already in section 10's pattern, so every
     published external link gets the cue with no authoring change. A class
     would have needed every old article revisited. Note what the rules this
     replaces hung off: `.link-theme` and `.link-article`, which are not
     external-specific at all, so had they ever rendered they would have put an
     external-link arrow on EVERY article link.

     Strictly the attribute means "opens in a new window" rather than
     "external", and that is the right reading rather than a compromise: it is
     what the contract's own `.sr-only` span says, so the glyph and the
     screen-reader text describe the same thing.

     THE GLYPH IS THE KIT'S OWN ICON, INSERTED BY external-link-icon.js, and it
     is `fa-regular fa-up-right-from-square` -- Joy's icon and Joy's style. It is
     not drawn here and cannot be: the Font Awesome kit is SVG+JS and ships no
     webfont, so the `content: '\f08e'` the dead rules used has no font for the
     codepoint to come from. A real <i> that the kit swaps for an <svg> is the
     one icon mechanism that works in this theme; that asset's header has the
     full reasoning, and callout-icons.js reached the same conclusion first.

     A MASKED SVG SHIPPED HERE FIRST, EARLIER THE SAME DAY, AND IS GONE.
     Masking is legitimate in this theme -- css-page-requests.css:464 draws the
     search glyph that way -- but it was wrong twice here. The shape was
     hand-drawn, i.e. an approximation of an icon the kit already ships. And its
     viewBox was `0 0 16 16` while the ink spanned only 1.75-14.25, so
     `mask-size: contain` rendered it at 78% of its box: a declared 0.75em drew
     at about 0.59em, which is what Joy saw as "way too small".

     SIZE AND COLOUR ARE BOTH SET HERE, AND COLOUR IS NOT OPTIONAL. The kit's
     svg carries `fill: currentColor`, so left alone the glyph follows the link's
     ink through every state and both theme tiers for free -- body ink in prose,
     the severity's ink inside a callout, --brand-text on hover. What it must not
     do is take `[class*='callout-'] svg` (css-components.css:1280), which paints
     the severity ICON colour at --step-5: an external link inside a callout
     would get a 25px glyph in the wrong ink. That rule is (0,1,1) and this is
     (0,3,1), so `color: inherit` and the size below are what hold it off. This
     is also why the <i> carries a class at all rather than being matched as a
     bare `svg` descendant.

     0.85em AND BASELINE-ALIGNED. `em` keeps it proportional to whatever text it
     follows, which matters because callouts run at --step-1 and body copy at
     --step-0. 0.85em IS A SETTLED VALUE, arrived at by trying both sides of it
     on 2026-10-28: 0.75em declared but drawing at 0.59em from the masked
     version was "way too small", and 1em -- where the kit draws the glyph at the
     full height of its font-size, so it stands clearly taller than the 0.7em
     capitals beside it -- was tried next and Joy came back down from it. So
     0.85em is a preference with both neighbours eliminated, not a first guess.
     It sits just above cap height, which is what reads as part of the line.

     `vertical-align: baseline` overrides the kit's own -0.125em,
     which would dip the glyph into the underline; baseline puts its bottom edge
     on the text baseline instead, so it reads against cap height and the 2px
     `text-underline-offset` from css-reset.css becomes a clear gap beneath it.
     The underline does run under the glyph and that is not avoidable -- the icon
     is a child of the link, and an ancestor's underline is painted across the
     whole inline box, atomic inlines included. `display: inline-flex` on the
     link is not a way out either: it is the thing `.link-article`'s
     `display: unset` exists to undo, because it stops a long link wrapping.

     The margin is a margin and not a space in the markup on purpose -- see the
     append note in external-link-icon.js: with no text node between the last
     word and the icon there is no break opportunity, so the glyph cannot be
     orphaned onto its own line when a long link wraps. */
  a[target='_blank'] .kb-external-icon {
    font-size: 0.85em;
    vertical-align: baseline;
    margin-inline-start: 0.25em;
    color: inherit;
  }

  h2 {
    font-size: var(--step-4);
    font-weight: var(--font-bold);
    text-wrap: pretty;
    line-height: 1.5;
    cursor: pointer;
    position: relative;
    display: flex;
    width: fit-content;

    &:hover {
      text-decoration: underline;
    }

    /* THE FOCUS RING. This replaced the last of the legacy salmon focus boxes on
       2026-10-29 -- a 2px --clr-primary-100 border, a #fce1d3 shadow, a 0.25rem
       radius and --space-3xs of padding. The h3 below carried an identical copy.
       Joy: "we need to remove the legacy focus states and institute our default
       border."

       THE BORDER AND THE PADDING WERE THE BUG, not just the colour: both are
       in-flow box properties, so tabbing to a heading grew its box by 2px of
       border plus --space-3xs of padding on all four sides and nudged the prose
       under it. An outline paints outside the border box and takes no space, so
       it cannot reflow anything -- which is the whole reason the sitewide cue is
       an outline, and the same swap css-page-header.css made for the search pill
       when that rule's twin came off on 2026-10-21.

       The ink was legacy twice over. --clr-primary-100 is the pre-refresh salmon
       and #fce1d3 was a raw hex on no tier at all, so this box had no dark mode
       -- it painted the same salmon on a dark surface. --form-focus-ring is the
       sitewide ring colour and has both tiers.

       `:has(.link-anchor:focus-visible)` RATHER THAN THE `:focus-within` IT
       REPLACES, and the two are not the same test. :focus-within follows plain
       :focus, so the old box also appeared on a MOUSE click -- and clicking is
       the normal way to use these headings, since .link-anchor::after below
       covers the whole heading as a copy-link target. So a reader who clicked a
       heading got a persistent salmon box around it until they clicked elsewhere.
       :focus-visible is the keyboard test and is what the rest of the theme
       keys to. Same selector shape as `.header-search.expand:has(input:focus-
       visible)`, for the same reason written there.

       The radius is declared here rather than on the heading because an outline
       follows `border-radius` and nothing else about these headings wants one.
       The offset is flat 2px, which is Codex's: the reset's default computes
       max(2px, 0.15em) and at --step-4 that would stand off nearly 5px. */
    &:has(.link-anchor:focus-visible) {
      outline: var(--border-width-heavy) solid var(--form-focus-ring);
      outline-offset: 2px;
      border-radius: var(--radius-sm);
    }

    /* ONE RING, NOT TWO -- the same pairing the header search uses. Still
       load-bearing, and more so than the comment here used to suggest: with
       `svg { display: none }` below, this anchor has no content and so no width,
       so the sitewide outline would draw it as a 2px hairline standing on its
       own beside the heading text. The cue belongs to the heading box, which is
       what a reader is aiming at. */
    .link-anchor:focus-visible {
      outline: none;
    }

    .link-anchor svg {
      display: none;
    }
    .link-anchor::after {
      content: '';
      position: absolute;
      inset-block-start: 0;
      inset-inline-start: 0;
      width: 100%;
      height: 100%;
      z-index: 10;
    }
  }
  h3 {
    font-size: var(--step-3);
    font-weight: var(--font-bold);
    line-height: 1.3;
    cursor: pointer;
    position: relative;
    display: flex;
    width: fit-content;

    &:hover {
      text-decoration: underline;
    }

    /* The h2's ring exactly, and the h2's `:focus-visible` comment holds all the
       reasoning -- why the border and the padding had to go rather than be
       retinted, why the salmon had no dark tier, and why this is keyed to
       `:has(... :focus-visible)` instead of the `:focus-within` it replaces.
       Declared twice rather than shared because the whole h2/h3 pair is written
       out twice in this file; folding just the focus state into one selector
       would be the only shared rule between them. */
    &:has(.link-anchor:focus-visible) {
      outline: var(--border-width-heavy) solid var(--form-focus-ring);
      outline-offset: 2px;
      border-radius: var(--radius-sm);
    }

    /* One ring, not two. See the h2's copy of this rule. */
    .link-anchor:focus-visible {
      outline: none;
    }

    .link-anchor svg {
      display: none;
    }
    .link-anchor::after {
      content: '';
      position: absolute;
      inset-block-start: 0;
      inset-inline-start: 0;
      width: 100%;
      height: 100%;
      z-index: 10;
    }
  }

  /* `.article-content .link:hover` is GONE, 2026-10-28. It painted
     --clr-secondary-5 (#fef2fe, a pale lilac with no dark tier) behind every
     classed link in an article body on hover -- not only the ones in callouts,
     which is how it read sitting above the severity rules below. It was the last
     copy of the background tint Joy killed sitewide on 2026-10-25, and it
     outlived that decision only because css-links.css had not been converted
     yet, so nothing had looked at this corner. The reason the tint is wrong is
     the same one css-reset.css:426 gives: a fill behind a wrapping inline link
     draws a ragged rectangle broken across the line. The hover is the ink and
     nothing else. */

  /* THE FOUR `.callout-* .link` RULES ARE GONE TOO, 2026-10-28, and with them
     the last thing in this theme that styled a callout link from outside the
     callout component. css-components.css:1337 already decides these -- a link
     takes its severity's ink and hovers to --brand-text -- and these four
     overrode that at (0,3,0) with legacy --clr-* inks and three more copies of
     the dead background tint.

     THEY WERE A DARK-MODE DEFECT, not a style difference. No --clr-* primitive
     has a dark tier, so each rule pinned one light-mode ink across both tiers,
     and dark callouts take the saturated strong fill:
       callout-tip       #1a5e63  6.77:1 light   4.39:1 dark
       callout-remember  #8f2a15  7.58:1 light   1.83:1 dark
       callout-warning   #66171e 10.67:1 light   1.99:1 dark
     `.callout-note` set no colour at all, only the tint, so a note link was
     already doing the right thing and the other three were not. --clr-success-140
     is also a teal, #1a5e63, which is not in the success hue family in either
     system -- so a tip's link did not even match its own callout.

     AND THEY BROKE THE HOVER OUTRIGHT. (0,3,0) beats both
     `[class*='callout-'] a` at (0,1,1) and its `a:hover` at (0,2,1), so a
     classed callout link got no colour change on hover whatsoever -- the only
     thing that moved was the tint, and that tint is the one killed sitewide on
     2026-10-25. Deleting these is what lets the component's own hover reach a
     classed link, which together with the --fg fix in css-links.css makes
     `class="link link-article"` and a bare <a> behave identically inside a
     callout. Light mode now clears AAA at rest and hover on all four severities
     (hover 7.37 / 7.46 / 7.42 / 7.10:1 after the --brand-text step raise).

     WHAT IS LEFT IS A DARK-MODE PROBLEM THIS PASS DOES NOT OWN. On a strong
     fill the rest ink is --fixed-white or --fixed-black, i.e. already at the
     contrast ceiling, so there is no colour for a hover to move to:
     --brand-text measures 4.63:1 on info, 2.69:1 on high, 3.64:1 on critical
     and 1.01:1 on success -- a hovered link on the lime strong panel is
     invisible. It is structural, not a wrong value; nothing on the ramp fixes
     it while the panel stays strong. Regular emphasis in dark would fix all
     four (7.93 / 5.38 / 8.24 / 9.22:1) and would lift two callouts' own body
     copy as well (remember 4.58 -> 9.14, warning 6.19 -> 7.77), but strong
     emphasis is Joy's explicit call from 2026-10-25 and reversing it is hers to
     make.

     SHE MADE IT ON 2026-10-28 AND THE ANSWER IS NO: the numbers above were put
     in front of her in the callout pass and she declined the trade -- "I say
     we've done what we can, so let's leave it at the current ink and we can
     adjust if need be." Dark callouts keep strong emphasis. This is no longer an
     open item, and it is not an accessibility gate either: the requirement on
     this project is AA, which these clear, with AAA as the target where it is
     reachable (Joy, same day: "AA is our requirement, but I like to hit AAA when
     at all possible"). Note that `remember` clears AA by 0.08 only, so if
     --high-bg-strong is ever changed, re-measure before shipping. The emphasis
     lever still exists if the design is revisited for other reasons; do not
     re-raise it as a defect. */

  /* Accordion
     ---------
     THE COMPONENT'S STRUCTURE AND VISUALS ARE ALL IN css-components.css NOW
     (2026-10-28, the Codex restyle). What was here was a SECOND, nested copy of
     .article-accordion, .article-accordion-label, its focus ring,
     `details[open] .article-accordion-label` and
     .article-accordion-content-wrapper -- the same selectors that file already
     declared, at (0,2,0) against its (0,1,0), so this file silently won every
     one of them and the other copy was decoration. Two homes for one component
     meant any value had to be changed twice to take effect.

     The split is now the one css-components.css:580 has always claimed: look
     there for the card, the rows, the chevron and the open/close animation, and
     keep only CONTENT SPACING here. The `details` rule below stays because it is
     scoped and deliberate -- see the tombstone at css-components.css:591 for why
     there must be no bare `details` rule anywhere. */

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

  .collapse {
    pointer-events: none;
  }

  details {
    display: flex;
    overflow: hidden;
    flex-direction: column;
  }

  /* Padding, the fill and the three borders all moved to css-components.css with
     the rest of the component. This stays because it is not a visual: the
     wrapper is a <div> and can land inside an <li>, where the general
     `li > * + *` rhythm would otherwise push the panel away from its own
     summary. */
  .article-accordion-content-wrapper {
    margin-block-start: 0;
  }

  /* Prevent the general li > div rule from adding top margin to the accordion
     content div, which would create unwanted space above the first element */
  .article-accordion-content {
    margin-block-start: 0;
  }

  /* THE PANEL'S TYPE IS A STEP BELOW THE ARTICLE'S, AND THE SUMMARY IS ITS
     CEILING (2026-10-28). Joy: "I don't want any headings to be larger than the
     title. For example, table captions or <h3>. Based on this, we would also
     scale down the <p> font size."

     The numbers this is fixing, measured on a live panel rather than derived --
     the summary title computes to 16px/600 and the h3 inside the panel to
     20.2464px, so authored content was outranking the control that owns it by
     27%. Captions were worse: `caption`/`figcaption` are --step-3 at :3667,
     the same 20.25px. Body copy came in at 16px, level with the title, because
     .article-content sets --step-1 and <p> simply inherits it.

     WHY A SINGLE font-size ON THE WRAPPER IS NOT ENOUGH, which is the thing
     that makes this four rules instead of one. Headings and captions do not
     inherit -- they are pinned to --step-* at (0,1,1) and (0,2,2) inside
     .article-content, and an element's own declaration beats an inherited value
     at any specificity. So each pinned group has to be re-pinned here. <p> and
     <li> do inherit and would have needed nothing, but they are named anyway
     because the ladder puts prose a step below the wrapper's own size rather
     than level with it.

     The ladder, settled 2026-10-28 over two rounds with Joy. The first version
     capped headings AT the title and she rejected it -- nothing inside the
     container should be as large as the title -- so every heading moved a full
     step under it and prose a step under the headings. That shape was right and
     the absolute sizes were not, so the WHOLE LADDER THEN MOVED UP ONE STEP,
     which is the version below. Keep the three rungs if you change it again:

       title (summary)   --step-2    (css-components.css) 17.72 -> 18.0px
       h3 .. h6          --step-3    -> --step-1    20.25       -> 16.0px
       captions          --step-3    -> --step-1    20.25       -> 16.0px
       p, li (inherited) --step-1    -> --step-0    16.0        -> 14.22px

     h3 is the top of the ladder because h1 and h2 cannot appear in a panel --
     the reasoning is at the selector itself, further down.

     THE STEP UP BOUGHT BACK TWO THINGS, both of which the lower ladder had
     cost. Prose is off the accessibility floor: it was --step--1, 12.64px,
     which css-root.css:143 names as the floor -- allowed, but with nothing left
     underneath it. And table cells are LEVEL with prose again: they are
     --step-0 at :3858 and deliberately outside this ladder, so on the lower
     version cell text sat a step above the copy around it and a list inside a
     cell sat a step below it. Both of those inversions are gone, which is why
     prose landing exactly on the wrapper's own --step-0 is a feature and not a
     coincidence -- see the note where the p/li rule used to be.

     :is() IS DOING SPECIFICITY WORK, not just shortening the selector. The
     caption group has to beat `.article-content figure.wysiwyg-table
     figcaption` at (0,2,2); :is() takes the HIGHEST specificity of its
     arguments, so listing the class alongside the two element names lifts the
     whole selector to (0,3,0) and it wins. Splitting these into separate
     selectors would quietly drop the figcaption half.

     NO line-height HERE, deliberately. The reset's 1.3 on headings and 1.5 on
     copy are ratios, so they follow the font-size down on their own -- the
     measured panel h3 was 20.2464px on a 26.3203px line, which is exactly
     1.3. Declaring line-heights would freeze what currently tracks. */
  .article-accordion-content {
    font-size: var(--step-0);
  }

  /* h1 AND h2 CAME OUT OF THIS SELECTOR ON 2026-10-28 (Joy: "h1s and h2s will
     never go within accordions"), and the authoring contract says so twice for
     two different reasons:

       h1 -- article bodies start at <h2> because the article title is the
             page's <h1> (article-authoring-contract.md:105), so this arm never
             had anything to match.
       h2 -- the table of contents and sticky nav are built from `.content h2`
             (:102-103), so an <h2> is the only element that mints a nav row.
             One inside a panel would put a rail entry on content that is
             collapsed until a reader opens it. It is a nav defect before it is
             a type problem, which is why no styling here could have fixed it.

     The contract does still say "any content is allowed inside" a panel
     (:147), so a stray <h2> is possible rather than impossible. What it loses
     is this rule's floor: it would render at --step-3, 20.25px, against a
     16px title. Accepted -- the nav row would be the louder symptom anyway.

     Specificity is unchanged by the removal: every arm was a type selector, so
     :is() still contributes (0,0,1) and the rule stays at (0,2,1). */
  /* SIZE ONLY, AND THE ABSENCE OF A COLOUR HERE IS THE DECISION. Panel headings
     keep the heading ink, which they get by inheritance-free fallback to
     `.article-content :is(h1 .. h6)` at :2885 -- --neutral-heading, #10171e
     light / #ebf0f4 dark, 17.89:1 and 12.73:1 on the panel's surface.

     They were briefly --neutral-icon on 2026-10-28 (Joy: every element in the
     panel on the prose colour, headings included) and she reversed it the same
     session. Recorded because the reversal is the informative half: a panel
     heading at prose colour is separated from its copy by weight and size only,
     and the heading ink is what keeps it reading as a heading. Do not re-apply
     it without asking.

     Nothing else in a panel needs an ink declaration either way --
     .article-content sets `color: var(--neutral-icon)` at :2863, prose inherits
     it, and `a:not([class])` is `color: inherit` at :2942, so links are
     prose-coloured and told apart by their underline. Captions are the one
     exception and have their own declaration at :3795. */
  .article-accordion-content :is(h3, h4, h5, h6) {
    font-size: var(--step-1);
  }

  .article-accordion-content :is(.article-table-caption, figcaption, caption) {
    font-size: var(--step-1);
  }

  /* THERE IS NO p/li RULE HERE ANY MORE and that is the point of the wrapper's
     --step-0 above. The ladder went up a step on 2026-10-28, which put prose on
     --step-0 -- exactly what the wrapper already declares -- so the rule that
     used to live here (`:is(p, li) { font-size: var(--step--1) }`) would now
     restate an inherited value and do nothing.

     IT IS SAFE TO RELY ON INHERITANCE FOR THESE TWO, checked rather than
     assumed: an element's own declaration beats an inherited value at any
     specificity, so this only holds while nothing declares their size in scope.
     Nothing does. The only `p` font-sizes in the theme are .article-vote p
     (:2223) and .article-vote-footer p (:2609), and the only `li` one is
     .hc-multiselect-toggle li (css-utilities.css:950) -- none of them can reach
     a panel. If a `.article-content p` rule is ever added, prose in here will
     silently follow it and this rule has to come back. */

  /* Accordion content spacing
     All spacing uses margin-block-start as the single source of truth.
     :first-child rules prevent extra space at the top of any container.
     margin-block-end on li > p is zeroed to prevent double-spacing when a <p>
     is followed by a callout div — the li > p selector specificity is required
     to override the same property in css-components.css (loads after this file).
     Callouts inside li elements have their global margin zeroed since li > * + *
     already provides the gap.

     THE GAP IS --space-2xs, 8px -> 12px (Joy, 2026-10-28). It was --space-s,
     16px -> 24px, in all four rules below -- the article's own flow rhythm,
     which inside a panel of reduced type read as a wide, loose stack. The panel
     is nested page flow rather than component chrome, so it stays on the
     fluid --space-* scale; it just sits two rungs lower on it now.
     Consecutive separate ul/ol blocks (a WYSIWYG quirk) get explicit spacing
     since ul > * + * only fires between siblings within the same list. */

  .article-accordion-content > :first-child,
  .article-accordion-content li > :first-child {
    margin-block-start: 0;
  }

  /* Remove top margin from first <p> inside any callout — the ol/ul p rule
     would otherwise add 1.5rem above it */
  [class*='callout-'] p:first-child {
    margin-block-start: 0;
  }

  .article-accordion-content > * + * {
    margin-block-start: var(--space-2xs);
  }

  .article-accordion-content li > * + * {
    margin-block-start: var(--space-2xs);
  }

  /* p elements inside li: no bottom margin by default to prevent double-spacing.
     When a p is directly followed by a callout, we restore a bottom margin on
     the p to provide the gap — and suppress the callout's own top margin so
     only one source of spacing applies. */
  .article-accordion-content li > p {
    margin-block-end: 0;
  }

  .article-accordion-content li > p:has(+ [class*='callout-']) {
    margin-block-end: var(--space-2xs);
  }

  .article-accordion-content
    li:has(> p + [class*='callout-'])
    > [class*='callout-'] {
    margin-block-start: 0;
  }

  .article-accordion-content ul + ul,
  .article-accordion-content ol + ol,
  .article-accordion-content ul + ol,
  .article-accordion-content ol + ul {
    margin-block-start: var(--space-2xs);
  }

  /* End Accordion */

  /* FLOW SPACING ONLY. The card itself -- fill, border, radius, clip, and the
     column -- is in css-components.css; these two are the component's place in
     the page's rhythm, which is why they are the one part of it still on the
     fluid --space-* scale.

     `justify-content: space-between` and `border-radius: 0.25rem` came out on
     2026-10-28. The radius is --radius-md on the card now, and the
     justify-content never did anything: the template gives .toggles a
     `flex-column` class, and space-between distributes along the main axis,
     which in a column of auto-height rows has no free space to distribute. */
  .toggles {
    margin-block-end: var(--space-s);
  }

  ol .toggles,
  ul .toggles {
    margin-block-start: var(--space-s);
  }

  /*!SECTION - Accordion */

  /*******************************/

  /* Lists */

  /* ARTICLE LISTS RUN ON THE BROWSER'S OWN DEFAULTS AND NOTHING ELSE,
     2026-10-28. Joy: "we are going to remove all custom styling from our lists
     and just go with how they come out of the box."

     That is less of a deletion than it sounds, because `css-reset.css` already
     took the defaults away sitewide -- `:243` zeroes margin and padding on
     every ol/ul and `:250` sets `list-style: none`. Simply deleting the rules
     this block replaces would have left article lists with no markers and no
     indent at all. The defaults have to be put back, not just stopped.

     `revert` is the right keyword and not `initial`. It rolls each property
     back past our author-origin reset to the UA stylesheet, so we inherit
     Chrome's and Firefox's own list rules wholesale: `disc` for ul and
     `decimal` for ol, their nested `circle`/`square` and nested-decimal
     selectors, their `padding-inline-start: 40px`, their mapping of the
     presentational `type="a"` attribute, and `::marker` in currentColor.
     `initial` would instead give `list-style-type: disc` on an ol and
     `padding: 0`, which is not "out of the box" -- it is a third thing.

     MARGIN IS DELIBERATELY NOT REVERTED. The UA puts `margin-block: 1em` on a
     list; `.article-content > * + *` (line 2710) already gives a top-level list
     1.5rem from the paragraph above it, and reverting would stack the two.
     The reset's zero is the correct value here.

     THE INDENT IS THE BROWSER'S 40px AND IS NOT OURS TO DEFEND. Four shapes
     were built and previewed on 2026-10-28 trying to improve on it, and the
     defaults outlasted all of them -- Joy: "weighing the benefits and costs I
     think the default is the way we'll go for the time being. If my team wants
     to look at it again, we can come back to this." What follows is that note,
     so the next attempt starts from the costs rather than from scratch.

     THE TWO THINGS ANYONE WILL WANT: markers flush with the surrounding
     paragraph text, and a smaller indent inside a table cell, where 40px in a
     narrow column is the one place the default genuinely reads badly. The
     candidates, in the order they were tried:

     1. ZERO THE TOP-LEVEL PADDING, outside markers hang in the gutter. Flush,
        and keeps a real hanging indent. Falls apart in a callout, which has no
        gutter -- what is left of the text there is the 1.25rem grid gap and
        then the severity icon, so an `a. b. c.` list gave the card two left
        edges. Also do not reach for `:not(li) > :is(ol, ul)` to mean "the top
        level": nested inside `.article-content`, a selector carrying no `&` is
        prepended with a DESCENDANT `&`, so it resolves to
        `.article-content :not(li) > :is(ol, ul)` and cannot match a list whose
        parent IS .article-content. That silently matched nothing and reached
        preview looking untouched. Zero everywhere plus `li > :is(ol, ul)` to
        put it back is the shape that works.

     2. `list-style-position: inside` in callouts only, on top of 1. Fixes the
        two left edges. Costs wrapped lines running back under the marker, and
        means the rule can never be written once -- body and callout want
        opposite declarations.

     3. NET-ZERO HANGING INDENT: `inside` with `padding-inline-start: 1.5em` and
        `text-indent: -1.5em` on the li. The best of the flush options, because
        nothing renders outside the list box at all, so one rule holds in prose,
        in a callout and in a cell. Two costs. `text-indent` inherits, so every
        block child of an item needs pinning back with `li > *` -- and that
        cannot help an item whose FIRST child is a block, since `inside` puts
        the marker in that child's own first line box, so a `<li><p>...` keeps
        its marker 1.5em in. And it goes ragged past nine items, because
        `inside` makes text follow the marker inline, so `10.` starts its first
        line further right than `1.` does.

     4. `padding-inline-start: 1.5em`, outside markers left alone. One
        declaration, no hack, and the even text column the others lose: the
        browser right-aligns an outside marker to the content edge, so `1.` and
        `10.` both end at 1.5em. Nested lists step a level at a time off their
        own padding. Gives up flushness -- the text column sits 1.5em in. Watch
        that a marker wider than the padding still overhangs, so a deep
        `lower-roman` (`viii.`) or a list past 99 items reaches outside the box.
        THIS IS THE ONE TO START FROM if the team reopens it: it is the whole
        fix for the table-cell complaint on its own, and the cheapest.

     THE GRID-PLUS-COUNTERS PATTERN IS THE OBVIOUS-LOOKING ANSWER AND CANNOT
     SHIP HERE. A two-column grid on the li with markers drawn as `::before`
     counters is the only shape that gets flush markers AND an even text column,
     which is why it keeps coming up. But an article body is authored in the
     Zendesk WYSIWYG, is a public API we cannot edit, and emits list types two
     ways -- `<ol type="a">` and `style="list-style-type: lower-latin"`, both at
     once in Joy's own repro. `content: counter()` always renders decimal; the
     attribute is recoverable with five selectors and the inline style
     effectively is not, so authored lists silently renumber. And because CSS
     Display only lets `list-item` combine with `flow`/`flow-root`, there is no
     `display: grid list-item` -- a grid item loses `::marker` necessarily,
     which takes list semantics with it and needs `role="list"` AND
     `role="listitem"` stamped on by JS over markup we do not control. One more
     trap in it: each li is its own grid, so a `max-content` marker track is
     sized per item and `9.` and `10.` indent their text differently. `subgrid`
     on the list fixes that; a fixed `ch` width does not, because `ch` is
     typeface-dependent and this refresh is mid font swap.

     KEEPING NATIVE MARKERS IS THE CONSTRAINT TO CHECK FIRST in any future
     attempt. It is what buys `type="a"`, inline `list-style-type`, the nested
     disc/circle/square and nested-decimal cascades, and list semantics, for
     free and without touching authored content. */
  ol,
  ul {
    list-style: revert;
    padding-inline-start: revert;
  }

  /* NOT part of the out-of-the-box pass. Out of the box there is no space
     between list items at all; this is the article's own block rhythm and Joy
     confirmed it the same day -- "I think we can hold to this tight spacing
     within callouts, and without we'll give more space." It was a flat 1.5rem
     until the rhythm pass on 2026-10-28 put it on --space-s, which is the same
     24px at 1440px and tightens to 16px on a phone. The tight counterpart for
     inside a callout is the Codex-spacing block below, and that one is
     deliberately NOT on the scale -- see the note there.

     TIGHTENED TO --space-xs ON 2026-10-28, Joy's ask after previewing the
     tables work: "I'd also like to tighten up the space between list items."
     24px -> 18px at the top of the range, 16px -> 12px at the bottom.

     THE `p` RULE BELOW MOVES WITH IT so the two stay in step: a paragraph
     inside a list item should not sit further from its neighbour than two
     list items sit from each other. Measured at 1028px on 2026-10-28, an item
     gap went 21.06px -> 15.79px.

     A `<li><p>text</p></li>` -- which the Zendesk editor does produce -- would
     collapse the p's margin with the li's and resolve to the larger of the
     two, so holding both on one token also keeps that markup honest. That is
     defensive, NOT the observed case: the test article's items measured
     `firstChild: text`, so nothing was collapsing there. */
  ul > * + *,
  ol > * + * {
    margin-block-start: var(--space-xs);
  }

  ol,
  ul {
    p {
      margin-block-start: var(--space-xs);
    }

    /* ...BUT NOT AT THE VERY TOP OF THE LIST. Writers author a lead-in
       paragraph INSIDE the list, before the first item:

           <ol type="a">
             <p>Testing list items</p>
             <li>List item</li>

       which is invalid -- `ol` takes only `li`, `script` and `template` -- but
       browsers keep it where it was put rather than hoisting it, so it renders
       and the rule above gives it a top margin. There is no preceding sibling
       for that margin to be spacing away from, so it just pushes the list's
       first line down. Joy caught it in a table cell on 2026-10-28, which is
       where it shows: a cell has padding rather than a collapsing edge, so the
       margin lands inside the cell and the paragraph sits ~18px below the
       first line of every cell beside it. The same markup in open prose is
       invisible, because there the gap reads as ordinary paragraph spacing.

       One source of spacing per gap, as everywhere else in this file: the gap
       above a list belongs to the list's own flow margin, never to its first
       child.

       The `li:first-child > p` arm is the `<li><p>text</p></li>` case the
       Zendesk editor emits. Unzeroed, that p's margin collapses up through the
       li and out to the list, becoming the list's effective top margin -- same
       symptom, one level deeper, and it is cheaper to cover here than to
       diagnose again. */
    & > p:first-child,
    & > li:first-child > p:first-child {
      margin-block-start: 0;
    }
  }

  /* A LIST INSIDE A CALLOUT RUNS ON CODEX'S SPACING, NOT THE ARTICLE'S, 2026-10-28.
     Joy, on the 1.5rem above: "We are definitely going to tighten up that
     1.5rem margin spacing. So let's start with Codex for everything and see
     where that takes us." 1.5rem between bullets is the right rhythm for a
     list in open prose and far too much inside a card -- it was what stranded
     the list in her tip-callout repro once the layout was fixed.

     Both numbers are Codex's Message list, read off the DevTools dump:
     `kb-message-list m-0 mt-1 list-none space-y-1 !pl-0`. `space-y-1` is the
     0.25rem between items and `mt-1` is the 0.25rem above the list, which
     stacks with the callout's own 0.25rem row gap (css-components.css, the
     Callouts block) to make the 8px title-to-first-bullet distance the
     reference screenshot actually shows. Flat values rather than --space-3xs
     for the reason given in full over there.

     IT LIVES HERE AND NOT WITH THE COMPONENT because of load order and
     specificity together. css-page-article.css loads after css-components.css
     (document_head.hbs:227 vs :220), so the `ul > * + *` rule above beats an
     equal-specificity override written in the component file; and inside
     `.article-content` this selector is (0,2,1) against that rule's (0,1,1).
     Either lever alone would have been enough, which is why this is not
     carrying an !important.

     `!pl-0` IS STILL THE ONE CODEX DECLARATION DELIBERATELY NOT COPIED, and
     the reason survived a day of trying to copy it. Codex sets `list-none` and
     draws its bullets as markup, so it owes its markers no space. Ours are
     native, and an indent is the only thing they have to render into: zeroing
     it pushes them out of the content column and, in a callout, under the
     severity icon. Joy asked the indent question directly on 2026-10-28 and the
     answer came back round to the browser's own 40px -- see the Lists section
     above for the four shapes tried and what each cost.

     THIS BLOCK IS THEREFORE SPACING ONLY, and a callout needs no indent rule of
     its own. For a few hours it also carried `list-style-position: inside` as a
     callout-specific patch, because the article had briefly zeroed the list
     indent and left outside markers hanging at the icon column while the rest
     of the callout's text stayed in the text column -- two left edges in one
     card. With the default indent back, that cannot happen and the patch is
     gone. */
  /* THESE TWO STAY FLAT AND MUST NOT GO ONTO THE --space-* SCALE. The rhythm
     pass on 2026-10-28 swept them onto --space-3xs and that was wrong: 3xs is
     `clamp(0.25rem, …, 0.375rem)`, so it holds 4px on a phone and grows to 6px
     on a desktop, which pulls the 8px title-to-first-bullet distance recorded
     above out of true at exactly the width the reference screenshot was taken
     at. Same argument as the field padding in css-components.css:3123 -- a
     component's internal spacing is flat because fluid values make it a
     different-looking component at the two ends of the range. The page's flow
     rhythm is the thing that should breathe, not the inside of a card. */
  [class*='callout-'] :is(ol, ul) {
    margin-block-start: 0.25rem;
    > * + * {
      margin-block-start: 0.25rem;
    }
  }

  ol,
  ul {
    [class*='callout-'] {
      padding: 0.5rem 0.75rem;
      border: none;
      display: inline-flex;
      /* `font-size: var(--size-0)` was here until 2026-10-26. --size-0 is not
         defined anywhere in the theme, and an undefined custom property in a
         font-size is not simply ignored: the declaration becomes invalid at
         computed-value time, which for an inherited property means `inherit`.
         So it was not dead, it was quietly overriding the base callout's
         `font-size: var(--step-1)` (css-components.css:1168) with whatever
         .article-content inherits from body -- 16px, no token involved.

         Deleted rather than corrected to --step-0, because 16px and --step-1's
         15.75-16px are the same size to within a rounding hair: the declaration
         was achieving nothing except coincidentally landing on the value the
         base rule already sets. Dropping it says that outright and changes
         nothing on screen. The table-cell block below had the same typo but a
         different parent, so it resolved somewhere real -- see the note there.

         If nested callouts are ever meant to step down a size deliberately,
         that is a design decision for both blocks at once, not a typo fix. */
      margin-block-end: unset;
      margin-block-start: var(--space-s);

      /* Hides the severity icon that callout-icons.js injects, and only that.
         It is inserted with insertBefore(icon, callout.firstChild), so it is
         always a direct first child of the callout -- which is what `>` pins
         here. Scoped on 2026-10-26, from `svg:first-child`: unqualified,
         :first-child matches an svg that is first inside anything, at any depth,
         so an inline icon an author opened their callout text with was being
         hidden too. Those are a documented part of the authoring contract
         (docs/brand-refresh/article-authoring-contract.md, section 12), so they
         are not ours to drop. The table-cell block below was worse and is
         corrected the same way. */
      > svg:first-child {
        display: none;
      }
    }
  }

  /* THE "LIST STYLE TYPES" BLOCK THAT STOOD HERE IS GONE, 2026-10-28, and this
     note is what replaces it. Roughly 110 lines of it were re-implementing the
     UA stylesheet by hand, so the `revert` pair above now does all of it:

       - `ul { disc }`, `:is(ul) :is(ul) { circle }` and a third level at
         `square` -- the browser's own ul/ul ul/ul ul ul cascade, exactly.
       - `:is(ol) :is(ol) { lower-latin }` and a third level at `lower-roman`.
         Note the default is nested DECIMAL, not latin; that is the one place
         the old code was making a real choice rather than copying, and
         "out of the box" means letting it go.
       - an `ol[type='a']` group restating what the presentational attribute
         already maps to. That group also carried a bare `[type='a']` selector,
         which matched any element in an article body with that attribute --
         `padding-left: 1rem` on an `<input type="a">` was never intended.
       - `::marker { color: var(--clr-neutral-160) }`, repeated five times.
         Markers now inherit currentColor, so they track the body ink in both
         tiers for free instead of being pinned to one light-tier neutral.
       - per-level `margin-inline-start` of 1.5rem/1rem, replaced by the
         reverted UA padding.

     ONE THING HERE WAS NOT A DEFAULT AND IS A DELIBERATE REMOVAL: ordered
     lists were `list-style: none` with the number drawn by hand as
     `ol li::before { content: counter(list-item) }` -- a 1.5rem bold glyph on a
     `--clr-neutral-5` fill, border-radius 50px, absolutely positioned into the
     indent. Those grey counter pills are what "just go with how they come out
     of the box" trades away for a plain `1.` in the body ink. It is the only
     visible loss in this pass, and it is the point of it. */

  li {
    /* THE GAP THAT OPENS A NESTED BLOCK, and it has to match the gap between
       items or the first sub-list item sits lower than every one after it.
       `ul > * + *` cannot reach these -- a nested list's parent is the `li`,
       not the list -- so when items moved to --space-xs on 2026-10-28 this was
       left behind on --space-s and opened a ~5px step above the first sub-item
       only. Joy caught it in preview: "extra space above first sub-list."
       Changed with it; the two are one rhythm and should move together.

       `div` rides along deliberately rather than being split off: a nested div
       opening wider than a nested list would be a difference with no reason
       behind it. The two wrappers that genuinely need their own value, code
       blocks and callouts, already override immediately below. */
    ul,
    ol,
    div {
      margin-block-start: var(--space-xs);
    }
    .code-block div {
      margin-block-start: 0;
    }
    [class*='callout-'] div {
      margin-block-start: 0;
    }
  }

  /***********************/

  /* Tables */

  /* TABLES TAKE THE FLOW RHYTHM LIKE ANY OTHER BLOCK, as of 2026-10-28. There
     used to be a `figure.wysiwyg-table, table > * + * { margin-block: 1.5rem
     !important }` here, and it is deleted rather than de-importanted:

       - It dates from the INITIAL COMMIT, not from this refresh. Nothing in the
         theme or in the vendored bundles declares a competing block margin on
         a table, so there is nothing for the `!important` to hold off -- it was
         belt-and-braces, and `git log -S` is how that was established rather
         than assumption.
       - Its `!important` is what forced the two inside `.tab-inner` below, which
         exist only to cancel it. Both lost theirs in the same change; at (0,3,1)
         and (0,4,1) they beat `.article-content > * + *` on specificity alone.
       - De-importanting was not enough on its own. `.article-content
         figure.wysiwyg-table` is (0,2,1) and the heading-adjacency rule at the
         top of this file is (0,1,2), so the figure rule would still have won and
         a table under a heading would still have sat at the full sibling gap
         instead of the tight one. Joy asked for tables to be "in alignment with
         our new system", and the only way there is for the system to govern them.

     What governs a table's flow margin now: `.article-table-scroll` is the
     element table-frame.js makes the direct child of `.article-content`, so it
     takes `--space-s` between siblings and `--space-2xs` directly after a
     heading, same as a paragraph. The `margin-inline: 0 !important` further
     down is a SEPARATE rule and keeps its `!important` -- that one is holding
     off author alignment, not flow spacing. */

  /* `.article-table-caption` is an authored <caption> that table-frame.js has
     lifted out of the table and placed above the frame -- see the CAPTION note
     in that file for why CSS cannot do it. It joins this list rather than
     getting a block of its own because it is the same thing in a different box:
     a label above a table, which is what `figcaption` already is here.

     `caption` STAYS IN THE LIST as the JavaScript-off fallback. With the script
     not running the caption is still inside the table, where this rule is the
     only thing styling it. */
  .article-table-caption,
  figure.wysiwyg-table figcaption,
  caption {
    text-align: start;
    margin-block-end: var(--space-2xs);
    font-weight: var(--font-bold);
    font-size: var(--step-3);
    /* NEW ON 2026-10-28, not a restoration -- a caption has never had a colour
       of its own here, so it has been inheriting --neutral-icon from
       .article-content:2863 along with the prose. Joy asked for captions on the
       heading ink everywhere, panels included, and this rule is the only home
       all three caption selectors share, so one declaration does it.

       It reaches inside an accordion because the panel's caption rule (:3331)
       only overrides font-size; nothing anywhere declares a caption colour to
       compete with. All four surfaces a caption can land on clear AAA with
       #10171e light / #ebf0f4 dark: the article card 17.89:1 / 12.73:1 (which
       is also the accordion panel, same --neutral-bg) and the table fill
       17.40:1 / 14.35:1. */
    color: var(--neutral-heading);
    padding: 0;
  }

  /* THE CAPTION'S OWN MARGIN HAS TO GOVERN THE GAP BELOW IT, and until this
     rule it did not. `.article-content > * + *` (:2740) gives every flow child
     `margin-block: var(--space-s)` -- a flat `1.5rem` when this was written,
     16->24px on the fluid scale since the 2026-10-28 rhythm pass -- so the
     hoisted caption's frame arrived carrying a 24px top margin, and the
     caption's 12px bottom margin COLLAPSED into it rather than adding to it, so
     24px simply won. Measured on 2026-10-27: 12px declared on the caption, 24px
     on the frame, 24px actual. The collapse is the same either way; below 1440px
     the frame's margin is now smaller, which only narrows the gap this closes.

     Zeroing the frame's top margin leaves --space-2xs as the only declared gap,
     which is the value the caption rule above already shares with figcaption.
     The frame's BOTTOM margin is deliberately left alone: that is the space
     between the table and whatever follows it, and the flow rule is the right
     owner of it.

     The adjacent sibling combinator is what keeps this to a captioned table
     rather than every framed one. At (0,3,0) inside `.article-content` it
     outranks the flow rule's (0,1,0) on specificity, so it needs no
     `!important` to do it. */
  .article-table-caption + .article-table-scroll {
    margin-block-start: 0;
  }

  /* The body fill goes on `table` and NOT on the selector group below, even
     though that group already lists `table` -- because it also lists
     `figure.wysiwyg-table`, and the figure wraps the <figcaption> as well as
     the table, so a fill there would paint a slab behind the caption too.
     Cells are transparent by default, so the table's own background is what
     shows through them. */
  table {
    background-color: var(--neutral-bg-component-soft);

    /* THE SEPARATED BORDER MODEL IS WHAT MAKES THE STICKY HEADER POSSIBLE, and
       it is the reason this overrides the reset (css-reset.css:489 sets
       `collapse` sitewide) for article tables only.

       Under `collapse`, a border between two cells belongs to the TABLE rather
       than to either cell. A `position: sticky` header cell then travels
       without its borders: the header's bottom rule and its column lines stay
       behind at the table's original geometry while the cell moves, so the
       pinned row loses its boundary and the body rows scroll visibly through
       the gap underneath it. There is a workaround -- redraw the header's edges
       as `box-shadow: inset`, which the cell does paint itself -- but it leaves
       the header's borders on a different mechanism from every other border in
       the table, so they round differently and can land a subpixel off at
       fractional zoom. The separated model avoids all of it: every border
       belongs to the cell that declares it, and a sticky cell takes its borders
       with it.

       `border-spacing: 0` is not optional here. The separated model's default
       spacing would open a 2px gap at every cell edge and let the table
       background show through as a grid of lines, which is both the wrong
       colour and not what the grid is meant to look like.

       WHAT THIS COSTS, and it is paid for below: the separated model IGNORES
       borders on rows and row groups entirely, so the one-declaration
       `tr { border-block-start }` divider this replaced cannot work any more and
       the dividers have to be declared on the cells. It also stops borders
       merging, so two adjacent declared borders now render as 2px of double
       line instead of collapsing to 1px -- which is why the cell rule below
       draws only two of each cell's four edges. */
    border-collapse: separate;
    border-spacing: 0;
  }

  /* THE ROUNDED, SHADOWED FRAME, and the reason it is on a wrapper that does
     not exist in authored markup. Codex's KbTable puts the border and the
     radius on an outer div rather than on the table, and that is forced rather
     than stylistic: a `display: table` box cannot be a scroll container
     (measured on the requests table, css-page-requests.css:616), and
     `border-collapse: collapse` from the reset makes a table ignore its own
     radius. The only element the WYSIWYG puts around a table is
     `figure.wysiwyg-table`, which also contains the <figcaption> -- so framing
     the figure would box the caption in with the grid and scroll it sideways.
     `assets/table-frame.js` inserts this wrapper at runtime instead, which
     leaves every published article's HTML exactly as written.

     Values are Codex's `hasBorder` variant: 1px --neutral-border, --radius-md.
     The shadow is the article card's own composite, and it sits here rather
     than on the table as Codex has it because this element clips -- and an
     element's own box-shadow is never clipped by its own overflow, only its
     descendants are.

     No `&:focus-visible { border }` guard is needed. The reset used to add a
     2px border on focus, which is what css-page-requests.css:630 is defending
     against, but that declaration was deleted on 2026-10-25 and only the
     outline remains. All this pins is Codex's flat offset, which the reset
     (css-reset.css:14) says components should pin for themselves. */
  .article-table-scroll {
    overflow: auto;
    overscroll-behavior-x: contain;

    /* THE FRAME HUGS THE TABLE, it does not stretch it. A plain block div would
       run the full width of the article card while an auto-width table inside
       it shrink-wraps to its content, leaving a two-column table sitting in a
       bordered box with a stretch of empty surface to its right -- which reads
       as a bug, not a design. `fit-content` sizes the frame to the table and
       then `max-inline-size` caps it, so a wide table fills the card and
       scrolls exactly as before.

       CODEX SOLVES THIS THE OTHER WAY and that option is one declaration if it
       is ever wanted: their table carries `min-w-full`, so the TABLE stretches
       to the wrapper instead of the wrapper shrinking to the table. It is not
       taken here because it would re-lay-out every narrow table in every
       published article -- the reset's `table-layout: fixed` (css-reset.css:488)
       would then divide the full card width equally between however many
       columns the writer happened to use. That is a change to existing content
       rather than to the frame, and it is not what was asked for. Swap to
       `min-inline-size: 100%` on the table above to get Codex's behaviour. */
    inline-size: fit-content;
    max-inline-size: 100%;
    max-block-size: 80vh;
    scrollbar-color: var(--neutral-border) var(--neutral-bg);
    border: var(--border-width-regular) solid var(--neutral-border);
    border-radius: var(--radius-md);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);

    &:focus-visible {
      --outline-offset: 2px;
    }
  }

  /* HIDING THE FIRST COLUMN'S LEFT BORDER, which is the one cost the grid rules
     below could not avoid. Every cell draws a `border-inline-start`, and with
     `rowspan` in play no selector can tell which cells are in column one (the
     reasoning is in full down at the cell rule), so the first column's line
     would otherwise sit 1px inside this frame's own border as a double rule.
     Joy asked for it gone on 2026-10-27.

     This nudges the table 1px toward inline-start instead, which puts that
     border column in the scroll container's UNREACHABLE overflow. A scroller's
     origin is fixed at its content box's start edge and there is no scrolling
     to a negative offset, so inline-start overflow is clipped and can never be
     brought into view -- unlike inline-end overflow, which is exactly what the
     scrollbar exists to reach. The 1px is spent, not hidden somewhere a reader
     can find it. Written as a logical margin so RTL (see the dir='rtl' rule
     earlier in this file) flips with it; the unreachable side flips too.

     The cost is 1px off the scrollable width, and for a table narrower than the
     frame, 1px of leftward shift. Both are below anything measurable.

     THE `!important` IS NOT OPTIONAL. The shared group below sets
     `margin-inline: 0 !important` on every table to beat the alignment margins
     CKEditor emits, and that shorthand covers inline-start. Important beats
     important on specificity, and `.article-table-scroll > table` (0,2,1) beats
     the group's `table` (0,1,1) -- so this wins, and only for a framed table. */
  .article-table-scroll > table {
    margin-inline-start: calc(var(--border-width-regular) * -1) !important;
  }

  /* THE NO-JAVASCRIPT FALLBACK, and it is horizontal only on purpose.
     `overflow-y: scroll` and `max-height: 80vh` used to sit in the group below,
     i.e. on the table as well as the figure. On the table they did nothing at
     all -- the same dead pair the requests table carried until someone measured
     it (css-page-requests.css:641) -- and dropping them from there is also what
     clears the way for the sticky header, since an `overflow` on any element
     between the header cell and the frame would capture the sticky and pin the
     header to the table instead of to the viewport of the scroller.

     What the figure still has to do is stop a wide table bursting the article
     card when the script has not run. Section 6 of the authoring contract warns
     writers off manual column widths precisely because they overflow on mobile,
     so that case is real and has to degrade.

     THE 80vh CAP IS DELIBERATELY NOT KEPT HERE, and my first version of this
     rule was wrong to claim nesting the two was harmless. The frame inside is
     capped at 80vh and the figcaption sits below it, so a capped figure would
     be asked to hold more than 80vh of content and would grow a SECOND
     scrollbar outside the first. Without the cap the figure always fits its
     contents, the frame is the only thing that scrolls, and with the script off
     a tall table simply makes a tall page -- which beats clipping it. */
  figure.wysiwyg-table,
  figure.wysiwyg-table-align-left,
  figure.wysiwyg-table-align-right {
    overflow: auto;
    scrollbar-color: var(--neutral-border) var(--neutral-bg);
  }

  figure.wysiwyg-table,
  figure.wysiwyg-table-align-left,
  figure.wysiwyg-table-align-right,
  table {
    max-width: 100%;
    font-size: var(--step-0);
    overflow-wrap: break-word;
    margin-inline: 0 !important;

    /* THE HEADER FILL HANGS OFF `th`, NOT OFF ROW POSITION, and that is a
       markup constraint rather than a preference. Writers author every table by
       hand in the Zendesk WYSIWYG, and the contract they were given (section 6
       of docs/brand-refresh/article-authoring-contract.md) marks header cells
       with `scope="col"` / `scope="row"` and never mentions <thead> at all. So
       a <thead> may or may not be in the output -- and the three rules that
       used to be here keyed off precisely that:

         thead > tr                   (0,2,3)  grey  --clr-neutral-5
         tbody > tr:nth-of-type(odd)  (0,3,3)  grey  --clr-neutral-5
         tr:first-child               (0,3,2)  white --clr-white

       WITH a <thead> the header row came out white, because tr:first-child
       outspecified the grey. WITHOUT one the first row came out zebra grey,
       because nth-of-type did. So the grey header fill those rules were written
       for could not paint in either shape -- it was dead code -- and the header
       rendered differently depending on markup nobody was ever asked to
       produce. Keying off `th` is the only form that survives both shapes, and
       it needs no change to what the WYSIWYG emits.

       A cell paints above its row (CSS tables paint table -> row groups ->
       rows -> cells), so a `th` fill covers whatever its row has. That also
       means a `scope="row"` header cell now takes the header fill: intentional,
       and the reason this is `th` and not `th[scope='col']`.

       Values are Codex's KbTable, read off the shipped component on 2026-10-27:
       thead is `bg-neutral-component`, tbody is `bg-neutral-component-soft`.
       Ink lands at 14.79:1 light / 9.56:1 dark in the header and 9.46:1 /
       9.12:1 in the body, AAA throughout. The white slab this replaces was
       1.81:1 in dark mode.

       THE SURFACES ARE CODEX'S; THE GRID IS NOT. Codex separates rows with a
       single soft top border and draws no verticals at all, which is what
       shipped here for the first few hours of 2026-10-27. Joy's team then asked
       for column lines and a sticky header, so the borders below are a
       deliberate divergence and the only part of this block that is. Do not
       "restore" it to match Codex without asking -- the zebra striping that
       came off at the same time was the half of that request nobody wants
       back. */
    /* A `td` IN A HEADER ROW IS A HEADER CELL, and the complex table in the
       authoring contract is exactly why that has to be said: its corner cell is
       authored `<td>&nbsp;</td>` and not a `th`, because there is nothing for it
       to be the heading of. Joy reported it on 2026-10-27 -- the corner rendered
       in the body fill while the two `scope="col"` cells beside it carried the
       header fill, so the header band had a hole punched in its first cell.

       `tr:has(th[scope='col']) > td` rather than `thead td`, for the same markup
       reason the fill hangs off `th` at all: section 6 of the contract marks
       header cells with `scope` and never mentions <thead>, so a header row may
       not be in one. Keying off a sibling `scope="col"` in the same row finds a
       header row wherever it sits. Body rows cannot match -- a `scope="row"` row
       has no `scope="col"` cell in it. */
    th,
    tr:has(th[scope='col']) > td {
      background-color: var(--neutral-bg-component);
      /* --neutral-heading, and it went to --neutral-icon and back on 2026-10-28
         -- Joy asked for one-colour tables, previewed it and reversed it the
         same session. The measurement is worth keeping because it is the reason
         not to try it again casually:

           --neutral-heading  #10171e on #deebec  14.79:1 AAA
                              #ebf0f4 on #2e3e4e   9.56:1 AAA
           --neutral-icon     #384649 on #deebec   8.04:1 AAA
                              #b2c3d2 on #2e3e4e   6.07:1 AA

         The prose ink drops the DARK header cell to AA. It clears the
         requirement and misses the 7:1 target, and #2e3e4e is the darkest
         surface a header cell sits on, so that is the floor of that option
         rather than an edge case. Body cells need no declaration at all either
         way -- they inherit --neutral-icon from .article-content:2863 -- so the
         contrast between a header cell and its column is carried entirely by
         this one line. */
      color: var(--neutral-heading);

      /* The grid lines on a header cell, set as a variable the `th, td` rule
         below reads rather than as a competing border declaration. A custom
         property resolves per element, so `th` gets the strong weight and `td`
         falls back to the soft one from a single pair of declarations -- no
         specificity contest and no source-order dependency to get wrong.

         It is needed because of a dark-tier token collision: --neutral-border-
         soft (#2e3e4e, css-brand-tokens.css:1048) is the SAME VALUE as this
         rule's --neutral-bg-component (#2e3e4e, :1034), so a soft border on a
         header cell was painted in exactly the colour of the fill under it and
         the header had no visible lines at all. Joy reported it on 2026-10-27;
         the light tier has no collision (#c9d7d9 on #deebec), which is why it
         showed in one tier only. Applies to every `th`, including
         `scope="row"`, since the fill above is unscoped too. */
      --table-line: var(--neutral-border);
    }

    /* TAKING BACK THE HAND-AUTHORED ZEBRA STRIPING, which turned out to be the
       other half of the striping request and the only half that could not be
       fixed by deleting a rule. Joy sent the markup on 2026-10-27: published
       tables carry `<tr style="background-color: var(--clr-neutral-5);">`
       alternating with `var(--clr-white)`, row by row, typed by writers into
       the WYSIWYG. The CSS striping came off earlier the same day and the
       tables stayed striped, because the stripes were never in CSS.

       THE DARK MODE CASE IS WHY THIS IS NOT COSMETIC. Both tokens are legacy
       `--clr-*` from the css-root.css block (--clr-white: #ffffff at :2,
       --clr-neutral-5: #f5f5f5 at :17) and NEITHER HAS A DARK TIER -- there is
       no `.dark-mode` override for them anywhere. So a dark-mode reader gets
       #ffffff and #f5f5f5 rows under light body ink: the white one measures
       1.81:1, against a 7:1 floor on this project. No amount of work on our
       own tokens reaches it, because the declaration is in the article.

       WHY `!important` IS THE RIGHT TOOL AND NOT A SMELL. An inline `style`
       attribute outranks every selector, however specific -- but it sits BELOW
       author `!important` in the cascade, so one important declaration in the
       theme beats it in every article at once. That is the only mechanism that
       does; nothing else in CSS can reach an inline declaration.

       WHY IT MATCHES ON `[style*='--clr-']` RATHER THAN ALL ROWS. A blanket
       `tr { background-color: transparent !important }` would be simpler and
       would also throw away a row a writer deliberately highlighted, with no
       way for them to get it back. Keying on the legacy token prefix hits the
       actual defect -- any inline reference to a token with no dark tier --
       and leaves a modern or hand-rolled value alone. It catches every
       `--clr-*`, not just these two, since the same no-dark-tier problem
       applies to all of them. Substring matching on `style` is reliable here
       because custom property names are case-sensitive, so there is no casing
       variant to miss; what it would miss is extra whitespace inside `var( )`,
       which CKEditor does not emit.

       `transparent` and not a token, so the fill stays defined in exactly one
       place: the table's own `--neutral-bg-component-soft` shows through. Rows
       paint below cells, so this cannot disturb a cell fill. */
    :is(tr, td)[style*='--clr-'] {
      background-color: transparent !important;
    }

    /* A header cell takes the header fill rather than going transparent. Same
       match, different value, because the sticky header has to stay opaque --
       a see-through `th` would let the body rows scroll visibly through it. */
    th[style*='--clr-'] {
      background-color: var(--neutral-bg-component) !important;
    }

    /* THE FULL GRID -- ROW DIVIDERS AND COLUMN LINES. Column lines came back on
       2026-10-27, asked for by Joy's team along with the sticky header: "get rid
       of the zebra striping, reinstate column lines and create the sticky
       header." Codex's KbTable has no verticals, so this is a deliberate
       divergence, and the case for it is the one the QA note in
       next-session.md was written around -- a complex table with both column
       and row headings is hard to track across without them, and section 6 of
       the authoring contract explicitly supports those.

       EACH CELL DRAWS TWO EDGES, NOT FOUR, and that is forced by the separated
       model above: borders no longer merge, so four-edge cells would render
       every interior line at 2px. Bottom and inline-start are the two chosen,
       because that pairing puts the header/body boundary on the HEADER's own
       bottom edge -- see `th[scope='col']` below, which upgrades that one
       declaration to the strong weight without anything else needing to move.
       Drawing top edges instead would have put a soft body border directly
       beneath the strong header border and doubled exactly the line that is
       supposed to read as a boundary.

       NOTHING IS ZEROED AT THE TABLE'S OUTER EDGES, and that is a correctness
       decision rather than an oversight. The obvious version of this block had
       two more rules -- `tr > :is(th, td):first-child { border-inline-start: 0 }`
       and a last-row equivalent -- so that the grid did not double the frame's
       border. Joy reported column lines appearing on some rows and not others
       on 2026-10-27, and that pair was the cause: with `rowspan` in play, DOM
       sibling position is NOT grid column position. A row that a span reaches
       into from above contains no cell in column one, so its first child is the
       column-two cell, and `:first-child` punched out the line between columns
       one and two on precisely the rows the span covered. The last-row rule had
       the mirror-image hole, missing any cell that span-ends on the final row.

       There is no selector that fixes this. `:first-child`, `:nth-child` and
       `+` all count DOM siblings, and no CSS selector can express a grid column
       index, so any position-based rule here is guessing. The authoring
       contract's section 6 invites exactly the tables that break it -- complex
       tables with both column and row headings -- so the rules had to go.

       WHAT IT COSTS: the first column's soft `border-inline-start` and the last
       row's soft `border-block-end` now sit 1px inside the frame's own border,
       where they read as a double rule. The LEFT one is dealt with up at
       `.article-table-scroll > table`, by nudging the table 1px into the
       scroller's unreachable overflow -- Joy asked for that on 2026-10-27.

       The bottom one is still there and has no equivalent trick: block-end
       overflow is the side a scrollbar reaches, so a negative margin there
       would move the line rather than hide it. A `tr:last-child` rule does not
       close it either -- a cell that span-ENDS on the final row is a child of
       whichever row it started in, so the rule would miss exactly the columns a
       span passes through. Left as-is until someone reports seeing it; the
       other routes out are worse. Moving the frame's outline onto the table
       leaves all four corners notched, because `overflow` + `border-radius`
       clips the table to the curve and only the frame's own border can draw a
       clean one. Painting over the cell lines with an inset `outline` on the
       frame works visually but is (0,2,0) against the reset's `*:focus-visible`
       at (0,1,0), so it would silently cost the wrapper its focus ring. */
    th,
    td {
      padding: var(--space-xs) var(--space-2xs);
      border-block-end: var(--border-width-regular) solid
        var(--table-line, var(--neutral-border-soft));
      border-inline-start: var(--border-width-regular) solid
        var(--table-line, var(--neutral-border-soft));

      .link-article {
        font-size: var(--step-0);
        display: inline;
      }
    }

    /* The corner `td` described at the fill rule above joins this one too, and
       it has to: with only the fill it keeps `position: static` while the cells
       beside it pin, so the first cell of the header row scrolls away on its own
       and the body rows show through the gap it leaves. The strong bottom
       border, the size and the block padding have to come with it for the same
       reason -- the row has to behave as one cell deep. */
    th[scope='col'],
    tr:has(th[scope='col']) > td {
      font-weight: var(--font-bold);

      /* A SMALLER, SHORTER HEADER, asked for by Joy on 2026-10-27 after seeing
         the complex table render: "decrease the font size and the header
         height/padding."

         --step-0, down from --step-1, and that is the floor of the scale -- the
         ramp in css-root.css runs --step-0 to --step-9 with nothing below it.
         It also lands on Codex's arrangement, whose KbTable head is
         `--font-size-sm`, the same size as its body text, with the weight above
         doing all the work of marking it as a header. The size was most of what
         made the header tall: at --step-1 a three-word heading like "Content
         Type and Title" wrapped to two lines in its column, so the height was
         two line boxes plus padding, and dropping a step buys back more height
         than the padding change below does by letting more headings sit on one
         line.

         PADDING-BLOCK ONLY, NEVER PADDING-INLINE, and that is not a style
         preference. The inline padding has to stay exactly equal to the body
         cells' in the `th, td` rule above, or the header label stops lining up
         with the content of its own column -- the one thing a column header
         has to do. Block padding is free to differ because nothing aligns
         across it. --space-2xs takes it from 12-18px a side to 8-12px. */
      font-size: var(--step-0);
      padding-block: var(--space-2xs);
      /* `start`, not `center`, as of 2026-10-27 -- Codex's KbTable head is
         `text-left` and so is the requests table (css-page-requests.css:694).
         The `!important` is kept: the contract tells writers nothing about cell
         alignment, so it is here to beat whatever CKEditor emits in an inline
         `style=`, exactly as the sibling `td` rule does. */
      text-align: start !important;

      /* THE HEADER/BODY BOUNDARY IS THE STRONG WEIGHT, NOT THE SOFT ONE, and
         that is a settled decision from another page rather than a new guess.
         Codex puts `border-t border-neutral-soft` on every row, so in their
         table the header/body boundary is just the first body row's top border
         -- the same weight as a divider between two ordinary rows. Joy looked at
         exactly that on the requests page on 2026-10-25 and asked for a bottom
         border on the top row, because it did not read as a boundary. The full
         reasoning is at css-page-requests.css:652; this matches it so the two
         tables do not disagree.

         So the table reads with three weights in order: outer edge and header
         rule at --neutral-border, row dividers quieter at --neutral-border-soft.

         This simply overwrites the soft `border-block-end` the cell rule above
         gave it, so there is exactly one declaration here and no doubled line
         to engineer around -- which is the whole reason that rule draws bottom
         edges rather than top ones. Only `scope='col'` gets it: a `scope='row'`
         header is a body cell, and a heavy rule under every one of those would
         read as a row of boundaries. */
      border-block-end: var(--border-width-regular) solid var(--neutral-border);

      /* THE STICKY HEADER, asked for by Joy's team on 2026-10-27. The scroll
         container it pins against is `.article-table-scroll`, so this does
         nothing until a table is tall enough to hit that frame's 80vh cap --
         which is the intent; a short table has nothing to scroll past.

         `th[scope='col']` AND NOT `thead`, for the markup reason this whole
         block turns on: the authoring contract never promises a <thead>, so a
         rule hung on one would pin nothing in most authored tables. Sticky on
         `thead` or `tr` is also the less portable choice even where the element
         exists -- cells have carried sticky far longer than row groups have.
         The flip side is that a <th> with no `scope` at all gets the header
         fill but does not pin. That follows the contract rather than guessing,
         and it is worth knowing if a writer reports a header that will not
         stick.

         The fill above is what makes it legible once pinned: a transparent
         sticky cell would let the body rows scroll through it. `z-index` is
         local and deliberately 1, not a step off Codex's scale -- the only
         thing it has to outrank is a positioned element inside a body cell,
         such as a callout, which would otherwise paint over the pinned row. It
         stays far below the page-level layers either way. */
      position: sticky;
      inset-block-start: 0;
      z-index: 1;

      /* Moved here off `tr:first-child` on 2026-10-27, for the same markup
         reason as the fill above: a scope="col" header is not necessarily in
         row one. It was already inert for `td`, which declares `vertical-align:
         top` on itself and so beats any value inherited from the row. */
      vertical-align: middle;
    }

    th[scope='rowgroup'],
    th[scope='row'] {
      text-align: start !important;
      font-weight: var(--font-bold);
      vertical-align: top;
    }

    /* FLAT, AND NOT ON THE --space-* SCALE. The rhythm pass on 2026-10-28 put
       this and the two list rules below onto --space-xs; Joy's call the same day
       was to take all three back to the flat 1rem they had. A table is a
       component and a cell is its inside, so this is the callout-list argument
       at css-page-article.css:3526 and the field padding at
       css-components.css:3123 applied once more: --space-xs is
       `clamp(0.75rem, …, 1.125rem)`, which would have made a cell TIGHTER than
       1rem on a phone and looser on a desktop -- a dense, narrow context moving
       in the wrong direction at the end that is already cramped. The page's flow
       rhythm breathes; the inside of a cell does not.

       The cell's own padding (--space-xs/--space-2xs at :4043, and the
       `!important` at :4385) predates the rhythm pass and is untouched here --
       by this same argument it is a candidate, but it was never part of the
       question Joy answered. */
    td > * + * {
      margin-block-start: 1rem;
    }

    td {
      vertical-align: top;
      font-size: var(--step-0);
      text-align: start !important;

      [class*='callout-'] {
        padding: 0.5rem 0.75rem;
        border: none;
        display: inline-flex;
        /* Was `var(--size-0)`, an undefined token, until 2026-10-26. As in the
           list block further up, that made the declaration invalid at
           computed-value time and so equivalent to `inherit` -- but here the
           parent is the `td` above, which sets --step-0 itself, so this one was
           already rendering at exactly --step-0. Naming it is therefore a
           pixel-for-pixel no-op that stops the size depending on a sibling
           rule's value. One step below the base callout's --step-1, which is
           reasonable inside a table cell and is why this is corrected where the
           list block's copy was deleted. */
        font-size: var(--step-0);

        /* THE CELL'S FLOW SPACING IS SINGLE-SOURCED on `td > * + *` above.
           This rule used to restate the cell's flow margin as well (`1rem`,
           briefly `var(--space-xs)` during the 2026-10-28 rhythm pass and flat
           1rem again after it), and at
           (0,3,2) against that rule's (0,2,2) it won -- so a callout authored
           as the FIRST thing in a cell got a top margin anyway and sat a line
           lower than the plain text in the cells beside it. Dropping the
           declaration hands the gap back to `td > * + *`, which applies it
           between siblings only: the same gap in every other position, and a
           leading callout now sits flush with the cell's own padding.

           `margin-block-end` stays unset. Nothing in the callout's own block
           (css-components.css:1165) sets a bottom margin, so this is defending
           against the article's prose rules reaching into cells rather than
           against the component -- and one source of spacing per gap is the
           whole point of the rule it defers to. */
        margin-block-end: unset;
        /* Was a bare `svg`, i.e. every svg anywhere inside a callout in a table
           cell. That hid the injected severity icon, which is the intent, and
           also every inline icon in the callout's own text, which is not: those
           are section 12 of the authoring contract and writers use a lot of
           them. Narrowed to the direct first child on 2026-10-26, which is
           exactly and only what callout-icons.js inserts. Same as the list
           block above. */
        > svg:first-child {
          display: none;
        }
      }
    }

    /*************************/
    /* Lists in tables */

    /* A tighter item rhythm than the body's is kept deliberately: a flat
       0.25rem against the --space-xs the body list gets, because a table cell
       is a narrow column and the body's rhythm strands bullets in it. FLAT for
       the reason given on `td > * + *` above -- these two went onto --space-xs
       in the rhythm pass and came back on Joy's call the same day.

       0.25rem, tightened from 1rem in three steps on 2026-10-28 -- Joy, on the
       first preview of the body change: "There's not much in the tables
       either," then on the 0.75rem that followed: "tighten up the list items a
       bit more," and finally, deciding it outright against a callout in
       preview: "Within tables, I think the spacing like we have in the callouts
       is the best."

       SO THIS IS THE SAME 4px AS THE CALLOUT LIST BLOCK, ON PURPOSE, and the
       two should move together if either is ever revisited -- see the
       Codex-spacing block at the Lists section above. The shared argument is
       that both are dense containers rather than open prose, and the shared
       mechanism is that both are FLAT rather than on --space-*. A cell now
       reads as tight as a card, which is what Joy chose. Only
       the INDENT rules went out of this block on 2026-10-28, with the rest of
       the custom list styling: `:is(ol, ul) { margin-inline-start: 1.5rem }`,
       its `p { margin-inline-start: 0 }` counterpart, which existed only to
       cancel it again inside a list item, and a `p + ul { 3rem }` that indented
       a list further for no reason beyond a paragraph preceding it. Indent is
       now owned in one place, by the `revert` pair in the Lists section, so a
       list in a cell starts flush with the cell's text exactly like one in the
       body does.

       A NARROW CELL IS THE ONE PLACE THE BROWSER'S 40px GENUINELY READS BADLY
       -- Joy: "inside tables the padding-inline can be an issue."

       THE CELL-ONLY INDENT IS BACK, 2026-10-28, on Joy's ask after previewing
       the flat spacing: "when a list is on its own in a cell, there is no
       inline-padding. When it follows a <p>, or if it is a sublist, it will
       have a smaller margin-inline padding." This is the one thing the sitewide
       park left on the table, and it is reopened HERE ONLY -- the sitewide
       default is untouched, so the team's parked decision still stands for
       every list outside a table.

       KNOWN COST, ACCEPTED: a cell-only indent is what this block used to
       carry, and keeping lists on one sitewide value is what had stopped cell
       and body lists from drifting. They can drift again now. The three
       declarations below are the whole mechanism, so that is the place to look
       if they do.

       WHY THE LONE LIST FLIPS TO `inside` AND THE OTHER TWO DO NOT. Markers are
       `outside` by default, which paints them LEFT of the content box; at a zero
       indent they hang into the cell's own inline padding, which is --space-2xs
       and so only 8px narrow / 12px wide. Cell text is --step-0, about 14px, at
       which an `a.` or `1.` marker runs 10-11px and `iii.` or `10.` more -- so
       at the NARROW end the marker is already wider than the 8px it has to hang
       in, and an outside marker at zero indent sits on or past the cell's left
       border in exactly the cramped column that prompted this. `inside` puts the marker in the first line box instead,
       where nothing can overflow. Joy chose that trade on 2026-10-28 knowing
       what it costs: a WRAPPED item runs back under its own marker rather than
       hanging, which is the common case in a ~160px column. The two indented
       cases keep `outside` because 2em is a real gutter for the marker to
       hang in, so they get an even text column for free.

       `list-style-position` INHERITS, which is why the sublist rule sets
       `outside` back explicitly -- without it a sublist inside a lone list
       would pick up `inside` from its ancestor and lose its gutter.

       2em, not a --space-* token, and that is deliberate: an indent has to
       scale with the TEXT it indents, not with the viewport. Against --step-0
       that is about 28px, down from the browser's 40px. It started at 1.5em
       (candidate 4's value from the Lists section above, ~21px) and went up on
       Joy's preview call the same day: "on sub-list in cell, let's put a little
       bit more of an indent." 1.5em is still the shape the team would start
       from if they ever reopen the sitewide default. */
    ul > * + *,
    ol > * + * {
      margin-block-start: 0.25rem;
    }

    :is(ol, ul) li:first-child {
      margin-block-start: 0;
    }

    /* (a) A list on its own in a cell -- flush, marker in the line box. Only
       direct children of the cell, so a sublist can never match it. */
    td > :is(ol, ul) {
      padding-inline-start: 0;
      list-style-position: inside;
    }

    /* (b) ...unless a paragraph introduces it, which makes it a nested thought
       rather than the cell's content. At (0,2,3) against the rule above's
       (0,2,2) this wins on specificity, not on order. */
    td > p + :is(ol, ul) {
      padding-inline-start: 2em;
      list-style-position: outside;
    }

    /* (b) ...and a sublist, at any depth. Carries the flow margin too.

       THE LEADING `td` IS THE th EXCLUSION, and it is the only one of the three
       indent rules that needed adding -- (a) and (b) above are already `td >`,
       but this one matched a sublist in a `th` as well until Joy confirmed on
       2026-10-28 that header cells should stay out. A `th` holds a column label,
       not authored prose; if a list ever appears in one it keeps the browser's
       40px, same as the parked sitewide default. Note this is a DESCENDANT `td`,
       not a child combinator, because a sublist is never a direct child of the
       cell. The flat 0.25rem flow spacing above is deliberately NOT scoped this
       way -- that predates the indent work and applies in both cell types. */
    td :is(ol, ul) :is(ol, ul) {
      margin-block-start: 0.25rem;
      padding-inline-start: 2em;
      list-style-position: outside;
    }
  }

  /* Tables in Tabs */

  .tab-inner {
    /* `.article-table-scroll:first-child` was added on 2026-10-27 alongside
       assets/table-frame.js, and without it this rule silently stopped working.
       A bare table in a tab panel used to BE the panel's first child; now the
       script's wrapper is, and the table is one level down -- so `table:
       first-child` still matched (the table is the wrapper's only child) but
       the margin it zeroed was no longer the one pushing the panel open. The
       wrapper is the element that now carries the flow margin from
       `.article-content > * + *`, so it is the element that has to be zeroed.
       Both selectors are kept: a table with JavaScript off is still a direct
       child here. */
    .article-table-scroll:first-child,
    table:first-child,
    figure:first-child {
      margin-block-start: 0;
    }

    figure.wysiwyg-table,
    figure.wysiwyg-table-align-left,
    figure.wysiwyg-table-align-right {
      margin-block-start: 0;
    }
  }
  /**********************/

  /* Images and video */
  img,
  iframe,
  video {
    border: 1px solid var(--clr-neutral-40);
    border-radius: 0.25rem;
  }

  video {
    display: block;
    width: 100%;
  }

  /* THE CODE BLOCK, round 1 of the `article_page` pass: SURFACES ONLY.
     Joy chose the Codex route on 2026-10-29 from three options -- a
     tier-following card, a dark slab kept in both tiers, and a tier-aware dark
     slab. What shipped before this was the second of those by accident rather
     than by decision: a hardcoded --clr-neutral-160 slab with an
     rgba(255,255,255,.1) hairline and an rgba(255,255,255,.9) ink, identical in
     both tiers, under a LIGHT --clr-neutral-10 header bar -- which is ΔE00 65.8
     off the slab it sat on and read as glare in dark mode.

     THE CODE BLOCK IS THE INVERSE OF THE TABS CASE, and that is why this is one
     declaration rather than a tier-split. Tabs had to sit RAISED off the page,
     and --neutral-bg-soft sank into our dark .article-container card, which is
     what forced the two-tier fill documented in the .nav-tabs block of
     css-components.css. A code block wants to sit RECESSED, and --neutral-bg-soft
     is recessed from the card by effectively the same amount in each tier:
     ΔE00 3.77 light (#ebf8f8 under a #faffff card), 3.27 dark (#172029 under a
     #1e2a35 card). So the same fill reads the same way in both, and no second
     token is needed.

     MEASURED, light / dark (AA 4.5:1 required, AAA 7:1 the target):
       code ink, --neutral-text on the fill     16.60:1 / 14.35:1   AAA both
       language label, --neutral-text-soft       6.13:1 /  9.12:1   AA light
       copy button at rest, same ink             6.13:1 /  9.12:1   AA light
       copy button hovered, --neutral-text      14.79:1 /  9.56:1   AAA both
       copy button copied, --success-text        5.89:1 / 12.47:1   AA light
       link hovered, --brand-text                7.53:1 /  9.68:1   AAA both
       1px --neutral-border on the fill          ΔE00 14.85 / 17.06  decorative
       scrollbar thumb on its track              8.04:1 /  4.79:1   clears 3:1

     The three light-tier AA rows are deviations by the same argument the
     inactive tab label makes: reaching AAA on them means --neutral-text, which
     is the code ink itself, and that would erase the one thing a muted ink
     exists to say. Logged in plan.md's Phase 1 deviations.

     THREE CODEX FEATURES DELIBERATELY NOT FAKED. shiki tokenising (§9 of
     article-authoring-contract.md is the whole contract -- Zendesk emits flat
     text in a single `code`, so there is nothing to colour and no tokenizer to
     colour it with), the line-number gutter (no per-line spans for the same
     reason), and the file-name header (authors have no field for it; the
     contract gives them a second `language-*` class instead, which is what the
     label already renders).

     WHAT THIS ROUND DOES NOT TOUCH, named so the next one does not have to
     rediscover it:
     - THE HEADER'S LAYOUT. Codex puts the language in a small
       `bg-neutral-component` BADGE inside a `px-4 py-2` header and moves the
       copy control out of the header entirely, to an icon-only button pinned
       `absolute right-2 bottom-2` over the code body, revealed on hover or
       keyboard focus. Ours is still a 200px uppercase "COPY TO CLIPBOARD"
       stretched to the header's full height, which is why the header keeps
       `padding: 0` and a `min-height` here. The badge cannot land without the
       button moving -- a chip needs the header's own inline padding, and that
       padding is exactly what `align-self: stretch` is living without. One
       decision, round 2.
     - TYPE. The body is still --step-1, which is BIGGER than prose; Codex's is
       `--font-size-sm`, smaller. Same round as the header.
     - THE FLOW MARGIN. `margin: var(--space-s) 0` restates what
       `.article-content > * + *` already gives this wrapper. It is redundant
       for a top-level block and load-bearing for one nested in a `td` or a
       callout, where the flow rule cannot reach it. The spacing pass closed on
       2026-10-28 and this is not a surfaces change, so it stays as found. */

  .zd-code-wrapper {
    position: relative;
    margin: var(--space-s) 0;
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--neutral-bg-soft);
    border: var(--border-width-regular) solid var(--neutral-border);
    font-family: var(--ff-mono);
  }

  /* A BORDER, NOT A FILL, and Codex's own reason for it is measurable: its
     header is `border-b border-neutral` over the same `bg-neutral-soft` as the
     body. A --neutral-bg-component bar instead would be ΔE00 2.76 from the body
     in light -- near-invisible -- and 9.86 in dark, which is loud. One
     declaration reading two different ways per tier is the thing to avoid, and
     the 1px --neutral-border does not do it: 14.85 light, 17.06 dark. Codex
     does still use the component fill, but on the language BADGE, where at
     chip size the dark figure reads as intent rather than glare -- see the
     round-2 note above. */
  /* THE HEADER NO LONGER EXISTS ON EVERY BLOCK. code-block-copy.js builds it
     only when detectLanguage finds a language, because once the copy button
     moved onto the code surface the badge became the header's only content and
     an undeclared language left a bordered strip with nothing in it. Most
     authors never pick a language, so that was the common case, not the edge.

     `min-height: 42px` is gone and the padding below replaces it: the pin was
     sizing a bar that had a 200px-wide stretched button in it, and the badge
     alone does not need propping to that height. Both values are FLAT, not
     --space-* -- a header bar is a component's inside, and only page flow rides
     the fluid scale (the spacing pass, 2026-10-28).

     KNOWN MISALIGNMENT, not fixed here: the 1rem inline padding does not line
     up with the code body's left edge, because the pre below uses
     `padding: var(--space-xs)`, which is fluid and lands anywhere from 12 to
     18px. The badge had the same 1rem before this round, so this is inherited
     rather than introduced. It resolves when the body padding does -- that is
     already queued as its own item, for the same insides-stay-flat reason. */
  .zd-code-header {
    display: flex;
    align-items: center;
    padding: 0.5rem 1rem;
    border-block-end: var(--border-width-regular) solid var(--neutral-border);
    font-family: var(--ff-mono);
    margin-block-start: 0;
  }

  /* PLAIN LOWERCASE TEXT, NOT A CHIP, which is Codex ("typescript") and which
     the measurement happens to agree with. It was uppercase, 600 weight, 0.1em
     tracked, and briefly on a --neutral-bg-component fill; dropping the fill
     puts the ink back on the code surface and IMPROVES it, from 5.46:1 light /
     6.07:1 dark on the chip to 6.13:1 / 9.12:1 here. Dark clears AAA outright
     and light is AA, short of the 7:1 target -- reaching it means
     --neutral-text, which is the code ink itself, and a label that matches the
     code it is labelling stops reading as a label. Same trade as the inactive
     tab, logged with it in plan.md Phase 1.

     The 0.1em tracking went with the uppercase rather than being kept
     separately. Tracking is what makes an uppercase micro-label legible; on
     lowercase it just reads as spaced-out text. That one is a judgment call
     rather than a measurement, so it is the easy thing to put back.

     No padding here any more -- the header's own 0.5rem/1rem gutter is the
     spacing now that there is no chip to size. */
  .zd-code-lang {
    font-size: var(--step--1);
    text-transform: lowercase;
    color: var(--neutral-text-soft);
    font-family: inherit;
    user-select: none;
  }

  /* AN ICON-ONLY BUTTON PINNED TO THE CODE SURFACE, which is Codex's. It was a
     200px `align-self: stretch` slab reading "COPY TO CLIPBOARD" inside the
     header; code-block-copy.js now appends it to .zd-code-wrapper instead, and
     it is positioned against that wrapper's `position: relative` so a long line
     of code scrolls underneath it rather than carrying it out of view. Dropped
     with the text: min-width, align-self, gap, text-align, justify-items,
     letter-spacing, text-transform, white-space and .zd-copy-inner, which had
     no remaining job once there was one child instead of two.

     `transition: all` WITH NO DURATION IS FIXED HERE, which was queued as its
     own defect and became unavoidable: `all` with the duration supplied only by
     :hover and .copied meant every state animated IN over 0.3s and snapped back
     OUT at 0s, and a hover-revealed button cannot fade in on a rule like that.
     Now a named list on --motion-duration-control, which reduced motion already
     takes to 0s in css-brand-tokens.css, so the fade respects it for free. The
     properties are named rather than `all` so the absolute insets and the
     outline are not swept in.

     FLAT 0.5rem INSETS AND FLAT PADDING on purpose: this is a control's own
     geometry, not page flow, so it does not ride the fluid --space-* scale.
     font-size is flat 1rem for the same reason -- it is not type here, it is
     the icon's size, and both icons track it (see ICON_CHECK's note in
     code-block-copy.js).

     The fill is ΔE00 2.76 from the code surface in light (1.12:1) and 9.86 in
     dark. Light is faint, and that is Codex's own figure, not a deviation we
     introduce -- their code surface is the same --neutral-bg-soft and their
     button the same --neutral-bg-component. It holds up because the button is
     identified by its ICON, at 14.79:1 light / 9.56:1 dark, and 1.4.11 asks for
     3:1 on a boundary only where the boundary is what identifies the control.
     The 1px --neutral-border-alpha-strong edge is Codex's too and is decorative
     by the same measurement: 1.51:1 light / 1.84:1 dark against the fill. */
  /* ONE PINNED ROW HOLDING THE STATUS TEXT AND THE BUTTON, which is what lets
     the confirmation sit beside a button that has to stay circular. A circle
     cannot hold the word "Copied!", so the text is a sibling rather than button
     content, and a flex row means its width never has to be guessed in a
     `calc()` against the button's offset -- the text simply pushes left.

     `pointer-events: none` with `auto` put back on the button alone, because
     this row spans more than the button: the gap and the full width of the
     status text would otherwise sit over the code as a dead strip that swallows
     a click-drag selection. Only the button needs to be hittable. */
  .zd-copy-affordance {
    position: absolute;
    inset-block-end: 0.75rem;
    inset-inline-end: 0.75rem;
    z-index: var(--z-index-raised);
    display: flex;
    align-items: center;
    gap: 0.5rem;
    pointer-events: none;
  }

  /* ALWAYS VISIBLE. This was revealed on hover inside a `(hover: hover)` block,
     which is Codex's behaviour but costs more than it buys here: a copy button
     nobody knows is there is a copy button nobody uses, and on a touch device
     -- where the media query never matched and the button showed anyway -- the
     two tiers of behaviour did not agree. Showing it always also removes the
     `:focus-visible` and `.copied` reveal rules that existed only to work
     around the hiding, and `opacity` leaves the transition list with them.

     A FIXED SQUARE BOX, not padding around an icon, and it is doing two jobs.

     The first is the second half of the flash fix. `padding: 0.375rem` on
     `line-height: 1` meant the button was sized by whatever its icon currently
     measured -- and the two icons do not match, since Font Awesome's copy glyph
     is 0.875em wide against the check's 1em. So the button changed width on
     every state change, and collapsed to bare padding for the frame when it had
     no icon at all. At a fixed 1.75rem it cannot do either, and 28px clears the
     24px minimum target size (WCAG 2.5.8) with room to spare.

     The second is the circle. --radius-round is 9999px, which clamps to half the
     shorter side, so it only draws as a true circle on a box whose two axes
     already agree -- on the old icon-sized box it would have been a stadium
     that changed shape with the glyph. The button is round in EVERY state now,
     which is Codex's, so the radius belongs here rather than on .copied.

     That leaves the transition list at two properties, and deliberately only
     two. `border-radius` came off it because the shape no longer changes, and
     `border-color` because no state changes that either -- the edge is one
     token in every state, so animating it was animating nothing. Success moves
     the fill and the ink; those are what is listed.

     The fill is ΔE00 2.76 from the code surface in light (1.12:1) and 9.86 in
     dark. Light is faint, and that is Codex's own figure, not a deviation we
     introduce -- their code surface is the same --neutral-bg-soft and their
     button the same --neutral-bg-component. It holds up because the button is
     identified by its ICON, at 14.79:1 light / 9.56:1 dark, and 1.4.11 asks for
     3:1 on a boundary only where the boundary is what identifies the control.
     The 1px --neutral-border-alpha-strong edge is Codex's too and is decorative
     by the same measurement: 1.51:1 light / 1.84:1 dark against the fill. */
  .zd-copy-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    padding: 0;
    pointer-events: auto;
    border: var(--border-width-regular) solid var(--neutral-border-alpha-strong);
    border-radius: var(--radius-round);
    background-color: var(--neutral-bg-component);
    color: var(--neutral-text);
    font-size: 1rem;
    font-family: inherit;
    line-height: 1;
    cursor: pointer;
    transition:
      background-color var(--motion-duration-control),
      color var(--motion-duration-control);
  }

  /* THE ICONS ARE BOTH IN THE BUTTON FROM THE START AND ONLY ONE IS DISPLAYED.
     Nothing is inserted or removed at click time, which is what stops the
     flash -- see the icon note in code-block-copy.js for the Font Awesome
     mechanism behind it.

     Targeted on `.fa-copy` rather than on a bare `i`, because the kit JS
     replaces the <i> with an <svg> and only the fa-* classes survive that swap.
     These selectors are (0,4,0) nested, so they beat the kit's own
     `.svg-inline--fa { display: inline-block }` at (0,1,0). */
  .zd-copy-btn .zd-copy-check {
    display: none;
  }

  .zd-copy-btn.copied .fa-copy {
    display: none;
  }

  .zd-copy-btn.copied .zd-copy-check {
    display: block;
  }

  /* The visible half of the confirmation, and plain text to match the language
     label rather than a second pill competing with the button beside it.
     --success-text sits on the code surface here, not on a fill, which is the
     better of the two: 5.89:1 light / 12.47:1 dark, against 5.25:1 / 8.30:1 on
     --neutral-bg-component. Light is AA, short of the 7:1 target, and the green
     is the point of it -- this is the one piece of text in the component whose
     job is to mean "success" rather than to be read neutrally.

     It fades rather than appearing, since unlike the icons there is no flash
     risk: the node is built once and empty, and only its text content changes.
     `:has()` carries .copied across from the button because the status is its
     PRECEDING sibling, which no sibling combinator can reach backwards to. */
  .zd-copy-status {
    font-size: var(--step--1);
    font-family: inherit;
    line-height: 1;
    white-space: nowrap;
    color: var(--success-text);
    opacity: 0;
    transition: opacity var(--motion-duration-control);
  }

  .zd-copy-affordance:has(.zd-copy-btn.copied) .zd-copy-status {
    opacity: 1;
  }

  /* The fill steps up and the ink stays put, which is what Codex's regular
     button variant does. Ink is already --neutral-text at rest now that the
     button has its own fill, so hover has nothing left to lift: 14.79:1 light /
     9.56:1 dark at rest, 13.58:1 / 6.84:1 hovered. Both tiers lose a little
     here and both stay well clear of AAA, which is the right direction for a
     state whose job is to say "this is the thing under your pointer". */
  .zd-copy-btn:hover {
    background-color: var(--neutral-bg-component-hover);
  }

  .zd-copy-btn:active {
    background-color: var(--neutral-bg-component-active);
  }

  /* THE INSET RING IS GONE AND SO IS THE WHOLE OVERRIDE, which the move earns
     twice over. It was `outline: none` plus an inset box-shadow, and the stated
     reason was the clip: the button used to be flush to the header's full
     height inside a wrapper that is `overflow: hidden`, so the sitewide outline
     was cut off along its top and right edges. The button now sits 0.75rem in
     from both edges it used to touch, and the sitewide ring extends 4px (2px
     wide, 2px offset) against 12px of clearance, so there is nothing left to
     clip.

     The second reason is the one that forces it. An INSET ring is drawn over
     the button's own fill, and this button's fill now changes in four states.
     Over --neutral-bg-component-active in dark the ring measures 1.79:1, which
     misses the 3:1 non-text floor outright -- a keyboard user holding Space
     would watch their own focus indicator fail. An OUTSET ring never touches
     the fill: both its edges sit on the code surface, which does not move, at
     4.25:1 light / 5.25:1 dark in every state the button can be in.

     So the rule reduces to pinning the offset. css-reset.css's *:focus-visible
     draws the ring sitewide and defaults the offset to max(2px, 0.15em); the
     flat 2px is Codex's figure, and pinning it is how the components that care
     already do this (see the accordion label and the tab list). This feeds the
     page-wide focus pass rather than pre-empting it -- the mechanism here had to
     change because the geometry changed under it. */
  .zd-copy-btn:focus-visible {
    --outline-offset: 2px;
  }

  /* THE SUCCESS STATE IS A FILL, NOT AN INK, and that correction is the whole
     rule. Reading Codex's success button as green CHECKMARK ink is what the
     earlier version of this block did -- hence --success-text on a neutral fill
     -- and the DevTools dump settles it: the class swaps kb-button-regular for
     kb-button-success, the background goes to the strong green, and icon_color
     stays rgb(16,23,30) in both states. Both of Codex's captured fills were
     hover variants because the pointer was still on the button when they were
     read, so the resting pair is --success-bg-strong / --neutral-bg-component
     and the hover pair is one step along from each.

     THE INK CANNOT BE --neutral-text AND THAT IS THE ONLY PLACE THE COPY BREAKS.
     Codex's literal declaration is dark ink on green, which is 12.00:1 in light
     -- and 1.47:1 in DARK, a flat fail, because --neutral-text flips near-white
     in the dark tier while --success-bg-strong stays a bright green (#98d95c).
     A fill that is bright in both tiers needs ink that is dark in both, and no
     single semantic token in the set does that. --neutral-text is #10171e in
     light and --neutral-text-inverse is #10171e in dark, so --zd-copy-ink takes
     one per tier and resolves to the same #10171e either way: 12.00:1 light /
     10.67:1 dark, AAA both. Hovered it is 8.57:1 / 14.28:1.

     A ratio cannot see this state change, because both fills sit on the same
     surface -- measured as ΔE00 against the rest fill it is 28.15 light / 66.07
     dark, so it reads unmistakably in both tiers even though the green itself is
     only 1.38:1 against the code surface in light. .copied is last so it beats
     :hover and :active on equal specificity, and the hovered variant is spelled
     out because a pointer that just clicked is still on the button.

     NO RADIUS AND NO BORDER COLOUR HERE, and both of those were mine to
     remove. The circle was briefly this rule's, on the reading that Codex only
     rounds the button on success; it is round at rest too, so the radius moved
     to .zd-copy-btn. The `border-color: var(--neutral-border-alpha-strong)`
     that sat here was a plain no-op -- it restated the token the base rule
     already sets, so it could not have changed a pixel in either tier.

     What it was reaching for is real but is NOT a defect, so it is recorded
     rather than fixed: --neutral-border-alpha-strong tier-flips from #10171e33
     to #ffffff33, while this fill stays a bright green in both tiers. So the
     copied button's edge is a dark alpha in light and a white alpha in dark,
     where the INK deliberately stays dark in both. Codex's captured border was
     rgba(16,23,30,0.2) in light and their dark success state was never read, so
     there is nothing to match against; the edge measures 1.51:1 light / 1.84:1
     dark against the fill either way, which is decorative by the same standard
     the base rule's note applies. Worth a second look only if the dark tier's
     lighter ring reads as a halo in preview. */
  .zd-copy-btn.copied {
    --zd-copy-ink: var(--neutral-text);
    background-color: var(--success-bg-strong);
    color: var(--zd-copy-ink);

    /* The dark half of the split. `:root[data-theme='dark'] &` with an explicit
       `&`, not a plain nested selector -- :root has to sit to the LEFT of this
       element, and a nested selector with no & gets a descendant & instead,
       which would look for a .zd-copy-btn.copied inside a :root inside the
       button and match nothing. Same form as the coloured cards in
       css-page-home.css. */
    :root[data-theme='dark'] & {
      --zd-copy-ink: var(--neutral-text-inverse);
    }
  }

  .zd-copy-btn.copied:hover {
    background-color: var(--success-bg-strong-hover);
  }

  /* THE FOURTEEN `!important`s ARE GONE AND NOTHING NEEDED THEM. What they were
     written against is `pre:has(code)` in css-reset.css, which hands every code
     block a --clr-neutral-5 fill, a --clr-neutral-160 border, 0.5rem of padding
     and a 0.25rem radius. That selector is (0,1,1). These rules are nested in
     `.article-content`, so they resolve to `.article-content .zd-code-wrapper
     pre` at (0,2,1) -- they already win on specificity, in a file that also
     loads after the reset. The one near-miss is the `pre` rule inside the
     callout block of css-components.css, also (0,2,1), and this file loading
     later settles that too.

     THE INK IS --neutral-text, NOT AN rgba WHITE. The old
     rgba(255,255,255,0.9) resolved to #ebebeb over the slab -- a fixed value
     that could only ever be read as light-on-dark, which is exactly why the
     block could not follow the tier.

     TWO DEAD RULES DELETED WITH IT, and they were dead by each other's hand.
     `pre.language-auto a` set an rgba-white ink and a --clr-secondary hover
     plum; `.zd-code-wrapper pre code *` then forced `color: inherit !important`
     over the top of them, and since the anchor is always inside the `code`, the
     important always won -- so a link in a code block has never once taken
     either colour. Removing both leaves links to the settled article behaviour:
     `a:not([class])` gives them `color: inherit`, which is now the code ink, and
     --brand-text on hover at 7.53:1 light / 9.68:1 dark.

     THAT RULE MUST STAY DELETED, and the reason has changed since it went. It
     was removed as harmless -- per §9 of the authoring contract Zendesk emits
     flat text in one `code`, so there were no token spans for it to flatten.
     There are now: prism.js wraps the code in .token spans and the syntax
     palette below colours them. `color: inherit !important` on
     `.zd-code-wrapper pre code *` would flatten every one of them back to the
     plain ink and silently delete syntax highlighting from the whole site. */
  .zd-code-wrapper pre {
    margin: 0;
    padding: var(--space-xs);
    /* ROOM RESERVED FOR THE COPY BUTTON, which only became necessary when the
       button stopped hiding itself. Hover-revealed, it covered the bottom-right
       of the code only while the pointer was over the block, so overlapping the
       last line did not matter. Permanently visible, it would sit on top of
       that line forever. 3rem is the button's own 1.75rem plus its 0.75rem
       inset plus 0.5rem of clearance, flat because all three of those are, and
       it is spelled as its own longhand so it overrides only the inline-end of
       the --space-xs shorthand above rather than replacing the padding.

       This cannot be made airtight and should not be mistaken for airtight: the
       pre is the horizontal scroll container, so padding reserves space at the
       END of the scrollable width. A line long enough to scroll still passes
       under the button mid-scroll. That is why the button carries a fill and a
       border rather than being a bare glyph -- it has to read as a layer over
       the code, not as part of it. */
    padding-inline-end: 3rem;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    color: var(--neutral-text);
    overflow-y: visible;
    overflow-x: auto;
    font-family: var(--ff-mono);
  }

  /* THE SCROLLER IS A TAB STOP, and that is the browser's doing rather than
     ours. Chrome gives any scroll container its own place in the tab order when
     it holds no keyboard-focusable children -- no tabindex needed -- so the
     `overflow-x: auto` above is the entire reason a reader can pan a wide block
     by keyboard. ONLY A BLOCK WIDE ENOUGH TO ACTUALLY SCROLL EARNS THE STOP,
     which is why Joy saw a focus cue on one code block and not on its
     neighbours (2026-10-29). code-block-copy.js already assumed this: its
     `focusin` listener on the pre expands a collapsed block, and nothing else
     in that block is focusable.

     SO THE RING GOES ON THE WRAPPER, NOT ON THE PRE. .zd-code-wrapper is
     `overflow: hidden` -- it has to be, to hold its radius -- and an outline
     paints OUTSIDE the border box, so the pre's own ring lost its inline-start,
     inline-end and block-end edges to the clip and survived as a stray rule
     across the top of the card, reading as a border nobody wrote. An element's
     overflow never clips its own outline, only its descendants', so moving the
     ring one level up draws it whole, at the card's radius, and says the true
     thing: what holds focus is the block. The framed table does this correctly
     already for the one reason that `.article-table-scroll` is not nested
     inside a clipper.

     `:has(pre:focus-visible)` RATHER THAN `:focus-within`, the same test as
     `.header-search.expand:has(input:focus-visible)` in css-page-header.css.
     :focus-within follows plain :focus, so it would also fire for the copy
     button and the collapse toggle -- both of which carry their own cue at
     .zd-copy-btn:focus-visible and .zd-code-toggle:focus-visible -- and ring
     the whole card a second time around them. */
  .zd-code-wrapper:has(pre:focus-visible) {
    outline: var(--border-width-heavy) solid var(--form-focus-ring);
    outline-offset: 2px;
  }

  /* ONE RING, NOT TWO: without this the pre's own outline still paints, clipped
     to the fragment the note above describes. */
  .zd-code-wrapper pre:focus-visible {
    outline: none;
  }

  /* TYPE, settled 2026-10-29 once Space Mono was on screen -- Joy signed off the
     typeface first ("looks great"), then took this recommendation, because
     Space Mono is markedly wider with a bigger x-height than Inconsolata and
     the right size is not judgeable from the numbers alone.

     --step-0, DOWN FROM --step-1, which had code rendering BIGGER than the prose
     around it. We were exactly inverted from Codex:

       Codex   body --font-size-base 16px   code --font-size-sm 14px   one DOWN
       ours    body --step-0       ~14px    code --step-1       ~16px  one UP

     Our base size is settled (c1d47a9), so the whole scale sits a step below
     Codex's and the two ways of following them diverge. Matching the RATIO means
     --step--1, ~12.4px. Matching the ABSOLUTE size means --step-0, ~14px, which
     is what Codex's 14px code block actually measures.

     --step--1, AND THAT IS THE RATIO ANSWER WINNING ON THE PREVIEW RATHER THAN
     ON THE ARGUMENT. This rule said --step-0 for about ten minutes, on the
     reasoning that 12.4px mono was "small enough to hurt". Joy looked at
     --step-0 rendered in Space Mono and asked for another step down, so the
     recommendation was wrong and the measurement is what settled it: Space Mono
     is wide with a large x-height, so it holds legibility at a size where
     Inconsolata would not have, and the face is what made the ratio choice
     viable. Worth keeping in mind for any future type call on this component --
     the arithmetic picked the wrong one of two defensible answers, and only the
     render could tell them apart.

     --step--1 is also what .zd-code-lang and .zd-copy-btn already use, so the
     whole component now sits on one step. Note the table block in this file
     claims --step-0 is the floor of the scale with nothing below it, which is
     wrong on both counts -- that page is signed off, so it is flagged rather
     than edited.

     A CODE BLOCK IN A TABLE CELL FOLLOWS THIS, which is worth stating because
     css-reset.css pins `td pre, td code` to --step-0 and that is NOT what wins:
     this rule is (0,2,2) against the reset's (0,0,2), so a block in a cell takes
     --step--1 like any other. The two were a step apart before this change and
     they are still a step apart, in the same direction; the cell pin is now the
     larger of the two rather than the smaller.

     LINE-HEIGHT 1.5, DOWN FROM 1.7, which is Codex's and is the ordinary value
     for code: 1.7 is prose leading, and prose leading on a monospace face reads
     as gappy because the glyphs are already wide. The line box goes from ~27px
     at the old --step-1/1.7 to ~19px here, so a long block loses close to a
     third of its height -- and the leading is the bigger half of that, not the
     size.

     THE BODY PADDING IS NOT TOUCHED AND IS A CANDIDATE. `padding: var(--space-xs)`
     on the `pre` above is a FLUID token on a component's inside, which is the
     one thing the spacing pass concluded not to do -- insides stay flat, page
     flow rides the scale. It ranges 12px to 18px with the viewport, so the block
     is differently proportioned at the two ends. The spacing pass closed on
     2026-10-28 and this is a type round, so it is named here rather than swept. */
  /* THE LINE HEIGHT IS A VARIABLE BECAUSE THE CLAMP HAS TO DO ARITHMETIC WITH
     IT. --zd-code-line-height is declared in the EXPANDABLE block below and
     read in two places: here, where it sets the actual leading, and there,
     where N lines are turned into a max-height. A literal in both would let
     them drift, and the symptom of that drift is a "Show more" that reveals a
     sliver or clips mid-glyph. */
  .zd-code-wrapper pre code {
    background: transparent;
    padding: 0;
    font-size: var(--step--1);
    line-height: var(--zd-code-line-height);
    color: var(--neutral-text);
    white-space: pre;
    font-family: var(--ff-mono);
  }

  /* ── SYNTAX PALETTE ───────────────────────────────────────────────────────
     FIVE INKS, EVERY ONE A TOKEN REFERENCE AND NOT ONE NEW HEX VALUE. The
     colours were first measured off Codex's own code block, which gave six
     slots; this is the palette rebuilt out of the brand ramps instead, which is
     what Joy asked for once she saw it was possible.

     WHY IT IS FIVE AND NOT SIX. Codex spends two separate inks on things we
     have to merge: a purple for functions at hue 308 and a violet-navy for
     variables and types at hue 295. Those are 13 degrees apart, and no palette
     ramp can supply two distinguishable hues that close together. Worse, at the
     7:1 contrast this needs on a light surface the palette has NO blue at all
     with any chroma left in it -- deep-teal has to go so dark to reach 7:1 on
     --neutral-bg-soft that it reads as black, not as blue. So the two collapse
     into one `ident` slot, which is the same merge Codex itself already makes
     between its variables and its types.

     WHICH RAMP GOT WHICH SLOT was searched rather than picked: every step of
     every ramp, filtered to contrast >= 7:1 on the code surface, chroma >= 28
     so the ink reads as a hue instead of a near-black, and L* inside the band
     Codex's own three sit in (25-32 light), then ranked by hue distance from
     the Codex reading and checked for mutual separation. Two things fell out of
     that which are worth recording because they look arbitrary otherwise:

     - FUNCTIONS ARE PINK, NOT PURPLE, and `ident` took the indigo. Codex has
       this the other way round. Giving indigo to `ident` puts it 5 degrees from
       Codex's navy instead of 9 from its purple, and it leaves aubergine for
       functions -- which is also what Prism's own default theme does (#DD4A68
       pink for function and class-name), so the result is conventional even
       though it is not a copy.
     - KEYWORDS ARE VERMILLION, NOT CHERRY, which is a worse hue match (41.9
       against Codex's 31.7, where cherry-12 is 37.6) and was still the right
       pick. Cherry and aubergine are 35 degrees apart in light but only 25 in
       dark, where both ramps have gone pale pink: cherry-3 against aubergine-3
       is ΔE00 10.4, close enough that keyword and function start to read as one
       colour. Vermillion moves away from aubergine rather than toward it and
       lifts the worst syntax pair to 13.7 in dark and 21.0 in light.

     ONLY THREE NAMES NEED A DARK VALUE. --code-string and --code-comment point
     at semantic tokens that already tier-flip on their own, so repeating them
     below would be a second copy of a decision made in css-brand-tokens.css.
     The three ramp primitives do not flip -- the dark tier redefines no
     --color-* name at all -- so each of those slots names its own step per
     tier, staying on ONE ramp across both. The hue family is the constant and
     the step is the tier's business, which is what a ramp is for.

     MEASURED, on --neutral-bg-soft (#ebf8f8 light / #172029 dark):
       keyword   vermillion-12 #992008  7.53:1  |  vermillion-2 #fdb5a6  9.68:1
       function  aubergine-11  #7c3149  8.07:1  |  aubergine-3  #f3afc6  9.22:1
       ident     indigo-11     #523f8e  7.85:1  |  indigo-2     #bdb0f2  8.36:1
       string    success-text  #476827  5.89:1  |  success-text #b2f27e 12.47:1
       comment   text-soft     #4f5f63  6.13:1  |  text-soft    #b2c3d2  9.12:1
       plain     neutral-text  #10171e 16.60:1  |  neutral-text #ebf0f4 14.35:1
     Nine of the twelve clear AAA. The three that do not are AA, all above 5.8:1,
     and all three are existing tokens used at their published value rather than
     anything chosen here -- comment and string in light, and comment is the
     lowest of the set at 6.13:1. */
  .zd-code-wrapper {
    --code-keyword: var(--color-vermillion-12);
    --code-function: var(--color-aubergine-11);
    --code-ident: var(--color-indigo-11);
    --code-string: var(--success-text);
    --code-comment: var(--neutral-text-soft);

    /* Explicit `&`, because a nested selector without one gets a DESCENDANT
       `&` -- this would otherwise look for a .zd-code-wrapper inside a :root
       inside the wrapper and match nothing, silently. */
    :root[data-theme='dark'] & {
      --code-keyword: var(--color-vermillion-2);
      --code-function: var(--color-aubergine-3);
      --code-ident: var(--color-indigo-2);
    }
  }

  /* WHAT IS DELIBERATELY NOT LISTED BELOW IS MOST OF PRISM. The vendored build
     emits 199 distinct token names across its 62 grammars, and anything without
     a rule here inherits the code block's own --neutral-text. That fallback is
     the design, not an omission: an unmapped token renders as plain code, which
     is correct-looking in every language, so a grammar can name whatever it
     likes without this file having to have anticipated it.

     PUNCTUATION AND OPERATORS ARE IN THAT UNMAPPED SET ON PURPOSE. Prism nests
     tokens -- markup's `tag` contains the angle brackets and the attr-name
     inside it -- so leaving punctuation unstyled lets brackets take the colour
     of whatever they belong to, which is what Prism's default theme does and
     what keeps a tag reading as one object. Pinning punctuation to plain ink
     instead would cut every tag and every selector into coloured and
     uncoloured halves.

     The groupings follow Prism's own default theme where it has an opinion,
     because that taxonomy is tested against all 62 grammars and a hand-made one
     would not be. The one place it is overruled is class-name: Prism colours it
     with functions, and it is in `ident` here, which is what Codex does and
     what makes a type read like the thing it names. */
  .zd-code-wrapper .token.comment,
  .zd-code-wrapper .token.prolog,
  .zd-code-wrapper .token.doctype,
  .zd-code-wrapper .token.cdata,
  .zd-code-wrapper .token.hashbang {
    color: var(--code-comment);
  }

  .zd-code-wrapper .token.keyword,
  .zd-code-wrapper .token.atrule,
  .zd-code-wrapper .token.important,
  .zd-code-wrapper .token.directive,
  .zd-code-wrapper .token.decorator,
  .zd-code-wrapper .token.annotation {
    color: var(--code-keyword);
  }

  .zd-code-wrapper .token.function,
  .zd-code-wrapper .token.function-name,
  .zd-code-wrapper .token.function-variable,
  .zd-code-wrapper .token.function-definition,
  .zd-code-wrapper .token.generic-function,
  .zd-code-wrapper .token.method-definition {
    color: var(--code-function);
  }

  /* The wide one -- names, types and the non-string literals. These are the
     tokens that carry what a snippet is ABOUT, and in markup, CSS and JSON they
     are nearly all of it: with tag, attr-name and property unstyled, an HTML
     block would come out almost monochrome. */
  .zd-code-wrapper .token.class-name,
  .zd-code-wrapper .token.type,
  .zd-code-wrapper .token.return-type,
  .zd-code-wrapper .token.generic,
  .zd-code-wrapper .token.builtin,
  .zd-code-wrapper .token.tag,
  .zd-code-wrapper .token.name,
  .zd-code-wrapper .token.attr-name,
  .zd-code-wrapper .token.property,
  .zd-code-wrapper .token.string-property,
  .zd-code-wrapper .token.literal-property,
  .zd-code-wrapper .token.key,
  .zd-code-wrapper .token.variable,
  .zd-code-wrapper .token.hvariable,
  .zd-code-wrapper .token.parameter,
  .zd-code-wrapper .token.constant,
  .zd-code-wrapper .token.symbol,
  .zd-code-wrapper .token.namespace,
  .zd-code-wrapper .token.number,
  .zd-code-wrapper .token.boolean,
  .zd-code-wrapper .token.null,
  .zd-code-wrapper .token.nil,
  .zd-code-wrapper .token.atom,
  .zd-code-wrapper .token.scalar,
  .zd-code-wrapper .token.entity,
  .zd-code-wrapper .token.url {
    color: var(--code-ident);
  }

  /* Every flavour of literal text. The long tail of *-string names is real --
     Python, Ruby, PHP, Rust and the shells each name their own quoting forms,
     and a heredoc that fell through to plain ink would be the one part of a
     shell script that stopped looking like data. `selector` and `regex` sit
     here for the same reason Prism's default puts them with strings: both are
     patterns being quoted at the language, not code being executed. */
  .zd-code-wrapper .token.string,
  .zd-code-wrapper .token.char,
  .zd-code-wrapper .token.attr-value,
  .zd-code-wrapper .token.regex,
  .zd-code-wrapper .token.selector,
  .zd-code-wrapper .token.template-string,
  .zd-code-wrapper .token.string-literal,
  .zd-code-wrapper .token.triple-quoted-string,
  .zd-code-wrapper .token.heredoc-string,
  .zd-code-wrapper .token.raw-string,
  .zd-code-wrapper .token.double-quoted-string,
  .zd-code-wrapper .token.single-quoted-string,
  .zd-code-wrapper .token.backtick-quoted-string {
    color: var(--code-string);
  }

  /* THE ONLY TWO NON-COLOUR RULES, and they are both markdown's. `**x**` inside
     a fenced markdown sample is bold text being quoted, so rendering it bold is
     showing what the source means rather than decorating it. This is not the
     bold Joy took off .zd-code-lang -- that one was a label shouting at the
     reader; these two only ever fire on markdown emphasis. */
  .zd-code-wrapper .token.bold {
    font-weight: bold;
  }
  .zd-code-wrapper .token.italic {
    font-style: italic;
  }

  /* Scrollbar styling for code blocks. Same conversion as the rules above and
     for the same reason: four rgba whites that could only read over a dark
     slab. A thumb is a control, so it is held to the 3:1 non-text floor against
     the track it moves along -- --neutral-border-strong on --neutral-bg-component
     is 8.04:1 light / 4.79:1 dark, and the hover pair is 5.46:1 / 6.07:1. The
     old values did not survive that test in light at all: 0.15 alpha white over
     a #333 slab is a thumb you can see, over a #ebf8f8 one it is nothing.

     THE TRACK IS THE SAME --neutral-bg-component THE BUTTON HOVERS TO, which
     the header rule above rejects for a permanent surface at ΔE00 2.76 in
     light. Here that near-invisibility is the point: the track should barely
     register and the thumb riding it should not. */
  .zd-code-wrapper pre::-webkit-scrollbar {
    height: 8px;
  }
  .zd-code-wrapper pre::-webkit-scrollbar-track {
    background: var(--neutral-bg-component);
  }
  .zd-code-wrapper pre::-webkit-scrollbar-thumb {
    background: var(--neutral-border-strong);
    border-radius: var(--radius-round);
  }
  .zd-code-wrapper pre::-webkit-scrollbar-track:hover {
    background: var(--neutral-bg-component-hover);
  }
  .zd-code-wrapper pre::-webkit-scrollbar-thumb:hover {
    background: var(--neutral-text-soft);
  }

  /* ── EXPANDABLE ───────────────────────────────────────────────────────────
     Codex clamps a long block, fades its lower edge and offers "Show more",
     with the control omitted when the code is short enough to show whole. All
     three of those are here. What is NOT here is Codex's way of asking for it:
     its KbCodeBlock takes a `maxLines` prop per block, and a Zendesk article
     body has no props. Per §9 of the authoring contract the author's whole
     vocabulary is a language picked from a CKEditor dropdown, so the limit is
     one sitewide number instead -- see code-block-copy.js's maxLines() for why
     that is the only shape available, and what it costs.

     THE NUMBER LIVES HERE AND NOWHERE ELSE. JS reads it back off the wrapper
     to decide whether to build the control. If it were declared in both
     places, a block between the two values would get a "Show more" that
     reveals nothing, or none when it needed one.

     15 lines is a judgment call, not a measurement -- about 280-320px tall
     depending on where --step--1 lands. It is long enough that a short
     example, which is most of them, is untouched, and short enough that a
     collapsed block does not dominate the page it is on. */
  .zd-code-wrapper {
    --zd-code-max-lines: 15;
    --zd-code-line-height: 1.5;
  }

  /* The positioning context for the fade and the copy row both. It replaces
     .zd-code-wrapper's own for those two -- the wrapper's `position: relative`
     stays, because the card still needs it. */
  .zd-code-body {
    position: relative;
  }

  /* THE PADDING TERM IS NOT OPTIONAL. `box-sizing: border-box` is global (see
     css-reset.css's universal rule), so a max-height on the `pre` is measured
     to the outside of its padding. Without `+ 2 * var(--space-xs)` the clamp
     would spend the pre's own top and bottom padding out of the 15 lines and
     show about 13 and a half of them.

     The last visible line lands flush against the fade rather than being half
     cut, which is deliberate: a clean edge under a gradient reads as "there is
     more", where a sliced row of glyphs reads as a rendering fault.

     This beats `overflow-y: visible` on `.zd-code-wrapper pre` on specificity
     -- two classes and a :not() against that rule's one class. */
  .zd-code-collapsible:not(.zd-code-expanded) .zd-code-body pre {
    max-height: calc(
      var(--zd-code-max-lines) * var(--zd-code-line-height) * var(--step--1) + 2 *
        var(--space-xs)
    );
    overflow-y: hidden;
  }

  /* THE FADE ENDS ON THE EXACT SURFACE BEHIND IT. --neutral-bg-soft is the
     wrapper's background and the pre's is `transparent`, so the gradient's far
     stop and the real surface are the same colour in both tiers -- no seam to
     line up, and the tier flip is handled by the token rather than by a second
     rule.

     `transparent` as the near stop is safe here. CSS gradients interpolate in
     premultiplied alpha, so transparent-to-colour does not drag the midpoint
     through grey the way the equivalent rgba(255,255,255,0) would.

     Two lines tall is enough to read as a fade and short enough that it never
     covers a whole line of code. It sits UNDER the copy button, which carries
     `z-index: var(--z-index-raised)` on .zd-copy-affordance, and
     `pointer-events: none` keeps it from eating a click meant for the code. */
  .zd-code-collapsible:not(.zd-code-expanded) .zd-code-body::after {
    content: '';
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: calc(2 * var(--zd-code-line-height) * var(--step--1));
    background: linear-gradient(to bottom, transparent, var(--neutral-bg-soft));
    pointer-events: none;
  }

  /* NO FILL ON THE FOOTER, AND THAT IS A MEASUREMENT RATHER THAN A TASTE. The
     focus ring has to be legible against whatever it is drawn on. The ring is
     outset at a 2px offset, so it lands on the footer's bare surface and never
     on the button's own fill: 4.25:1 light / 5.25:1 dark, clearing the 3:1
     non-text floor in both tiers. Giving the FOOTER the component fill instead
     would put the ring on 3.48:1 / 2.50:1, and dark fails. That the fill
     belongs on the button and not on the bar behind it is the whole reason
     this measures safely -- it is also the same correction that moved the copy
     button's ring from inset to outset.

     The divider mirrors the header's `border-block-end`, same token on the
     same surface, so a card with both is symmetrical. It measures 1.86:1 light
     / 2.10:1 dark, under 3:1, and that is acceptable for the same reason it is
     acceptable on the header: it is a divider, not the boundary that
     identifies a control. The control is identified by its text.

     The padding is FLAT, not --space-*: a footer bar is a component's inside
     and only page flow rides the fluid scale (the spacing pass, 2026-10-28).

     0.75rem IS THE SHADOW'S CLEARANCE, not a round number. The wrapper is
     `overflow: hidden` -- it has to be, to hold its radius -- so anything the
     button paints outside itself gets clipped, which is the hazard
     css-reset.css catalogues for focus rings. The hover shadow below is
     `0 6px 16px`, reaching 6 + 16/2 = 14px past the border box, so 12px of
     padding clips its outermost 2px. That is deliberate rather than missed:
     14px of padding to save 2px of a 10%-alpha blur would make the bar
     visibly bottom-heavy, and those 2px are the faintest part of the
     gradient. The 2px focus ring at its 2px offset needs 4px and is clear. */
  .zd-code-footer {
    display: flex;
    justify-content: center;
    padding: 0.75rem;
    border-block-start: var(--border-width-regular) solid var(--neutral-border);
  }

  /* CODEX'S `kb-button-sm`, EMPHASIS REGULAR -- the same control Codex puts in
     this exact position, transcribed from a DevTools dump of
     `.kb-button` + `.kb-button-sm` + `.kb-button-regular` rather than guessed.
     Codex's own declarations resolve to: pill radius, --font-family-primary,
     --font-weight-strong, `line-height: 1`, --font-size-xs, 8px of padding,
     a 1px --neutral-border-alpha-strong rim, --neutral-bg-component and the
     regular shadow.

     THE DUMP CONTAINS TWO COMPETING PADDINGS and the render settles which won.
     `.kb-button-sm` declares `padding: calc(var(--spacing) * 2)` (8px) while
     `.kb-button` in @layer utilities declares `padding-block: *3` /
     `padding-inline: *4` (12px / 16px). Measuring Codex's shipped button gives
     79x29px outer at a 1px border, which is 1 + 8 + 12 + 8 + 1 = 30 tall
     against 8px, and 38 against 12px. So the sm padding wins -- unlayered
     beating a layer, the ordinary cascade -- and it is 8px on all four sides.
     --spacing is Tailwind's 0.25rem base and is not a token we have, so the
     literal rem is written out, which is also what the four existing copies of
     this recipe do.

     THIS IS THE FIFTH COPY OF CODEX'S REGULAR BUTTON in the theme --
     .back-to-top-link (css-components.css), .kb-error-home
     (css-page-error.css), .kb-footer-theme (css-page-footer.css) and the
     header's locale chip are the others, and all four are the `md` size.
     Extracting one shared .kb-button-regular is still the button migration's
     job (docs/brand-refresh/plan.md), not this file's -- the same note
     .kb-error-home carries. If that recipe is retuned, this changes with it.

     --font-family-primary is Codex's Sora and carries no fallbacks of its own,
     hence the two added here; that is css-buttons.css's `.btn` idiom verbatim.
     It has to be stated at all because .zd-code-wrapper forces --ff-mono on
     everything inside it, and this label is interface text, not code.

     Font size is --step--1, not Codex's --font-size-xs. The two are 12.4px and
     12px, and the theme's documented decision is to keep KB4's step scale
     rather than port Codex's three custom sizes (css-brand-tokens.css header).
     It also matches .zd-code-lang and .zd-copy-btn, the component's other two
     bits of chrome.

     INK CLEARS AA IN EVERY STATE, both tiers: 14.79:1 light / 9.56:1 dark at
     rest, 13.58:1 / 6.84:1 hovered, 12.21:1 / 4.89:1 pressed. The two dark
     figures fall short of the 7:1 target and that is the floor of the ramp,
     not a tuning choice -- the fill has to keep stepping to read as a state
     change at all. AA is the requirement, AAA the target where reachable.

     THE FILL IS NOT WHAT MAKES THE PILL VISIBLE IN LIGHT MODE. Against the
     footer surface it is ΔE00 2.76 -- the same near-invisible figure the
     header rule above rejects a component-fill bar for, and 9.86 in dark.
     Here the rim and the shadow do that work instead, which is why this is a
     fill-plus-border-plus-shadow recipe and the header bar is a bare border.
     Same reason the rest -> hover step survives light mode at only ΔE00 2.08:
     the shadow growing from 2px/6px to 6px/16px is the perceptible half of
     that state, not the colour. */
  .zd-code-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    max-inline-size: 100%;
    padding: 0.5rem;
    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-text);
    font-family: var(--font-family-primary), system-ui, sans-serif;
    font-size: var(--step--1);
    font-weight: var(--font-weight-strong);
    line-height: 1;
    cursor: pointer;

    /* Tokens, where .back-to-top-link's copy of this ramp hardcodes
       `200ms ease-in-out`. --motion-duration-control IS 0.2s, so this renders
       identically -- but the token drops to 0s under the reduced-motion block
       in css-brand-tokens.css and the literal does not, so the hardcoded copy
       animates for readers who asked it not to. */
    transition:
      background-color var(--motion-duration-control) var(--ease-in-out),
      box-shadow var(--motion-duration-control) var(--ease-in-out),
      color var(--motion-duration-control) var(--ease-in-out);
  }

  /* Codex's `.kb-button-regular:hover`: a background step plus a taller,
     softer shadow. --shadow-hover is the same colour as --shadow-regular, so
     only the geometry changes -- 2px/6px becomes 6px/16px, which reads as the
     button rising. Derived once at css-components.css's .back-to-top-link:hover
     and not re-derived here. */
  .zd-code-toggle:hover {
    background: var(--neutral-bg-component-hover);
    box-shadow: 0 var(--shadow-y-offset-hover) var(--shadow-blur-hover) 0
      var(--shadow-hover);
  }

  /* Codex drops the shadow outright on press rather than returning it to the
     resting 2px/6px, so the press is the exact inverse of the hover lift:
     raised, to flat, to below flat. */
  .zd-code-toggle:active {
    background: var(--neutral-bg-component-active);
    box-shadow: none;
  }

  /* ORDERED AFTER :hover AND :active DELIBERATELY. All three are the same
     specificity, so source order is what lets the ring survive on a button
     that is hovered and focused at once.

     The ring itself is the sitewide one from css-reset.css's *:focus-visible;
     only the offset is set here. Dropping the shadow is the same call
     .back-to-top-link:focus-visible makes -- a 16px blur reading through a 2px
     ring muddies it, and the ring is the more important of the two. */
  .zd-code-toggle:focus-visible {
    --outline-offset: 2px;
    box-shadow: none;
  }

  /* NOTHING ANIMATES THE HEIGHT, and that is a constraint rather than an
     omission. A transition needs two resolvable lengths, and the expanded
     state is `auto`; the way around that is to measure scrollHeight and write
     a pixel height inline, which puts an inline style back on this component.
     The copy button's fade was removed for exactly that reason -- an inline
     style beats every stylesheet rule this component will ever want. A
     snap is the honest trade, and reduced-motion readers get it for free. */

  /* ── Keyboard keys ──────────────────────────────────────────────────────── */

  /* A KEYCAP, TAKEN FROM CODEX'S KbKbd AND OFF THE LEGACY PALETTE. What draws
     a `kbd` today comes from css-reset.css, which gives every one in the theme
     a --clr-neutral-5 fill, a --clr-neutral-160 border and a two-layer rgba
     shadow faking a 3D key -- pre-refresh values with no token behind them.
     This rule is (0,1,1) against that block's (0,0,1), so it wins on
     specificity and needs no !important. The reset's block is left standing:
     no template emits a `kbd`, so the only ones on the site are in article
     bodies and this covers them, and rewriting it sitewide is not this page's
     call to make.

     THE BORDER IS WHAT MAKES THIS READ AS A CAP, NOT THE FILL, and that is a
     measurement rather than a preference. --neutral-bg-soft against the page is
     only ΔE00 3.77 light / 2.97 dark, and in the corners -- where `main`'s
     radial gradients darken it to --page-bg-corner -- the light tier falls to
     1.32, under a just-noticeable difference, so the fill alone would vanish
     exactly there. --neutral-border measures ΔE00 17.01 / 19.95 against the
     page middle and still holds 13.61 / 13.86 in those corners. Same division
     of labour the Show more toggle arrived at: the rim and the shadow do the
     work and the fill is a tint.

     That border is 2.01:1 light / 2.30:1 dark, under the 3:1 non-text floor,
     and it is acceptable here for the reason the table header's divider is --
     a `kbd` is text, not a control, so nothing is identified by its boundary.
     The ink carries the meaning and is AAA in both tiers: --neutral-text on
     --neutral-bg-soft is 16.60:1 / 14.35:1, the same pair the code block
     already measures.

     NO FIXED HEIGHT, which is where this parts company with Codex. Its cap is
     `h-6`, a flat 24px. Ours is built from its type and its padding instead,
     so it tracks the text it is set in rather than being a number that only
     suits one context.

     TWO STEPS DOWN, AND MORE PADDING THAN CODEX, both Joy's calls off the
     preview. `calc(1em / 1.125 / 1.125)` resolves to --step--1 exactly at both
     ends of the clamp in body prose, because 1.125 is the ratio between our
     steps -- so the keycap, the inline chip and the code block all sit on one
     step now. That is the third time Space Mono has wanted a size below what
     the arithmetic recommended. Relative rather than pinned so a key in an h2
     scales with the heading instead of shrinking to body size, and --step--1
     is the floor of the scale, so this lands ON it rather than under it.

     THE NEGATIVE MARGIN IS WHAT BUYS THE PADDING, and without it the padding
     she asked for was not available at this type size. A `kbd` is
     `inline-flex`, which makes it an atomic inline-level box: its MARGIN box
     sets the height of the line box it sits in, so vertical padding on it
     pushes the whole line apart -- unlike the inline chip below, where padding
     grows the painted box without touching the line at all. Prose here is
     --step-1 at line-height 1.5, a 23.6px line box, and 12.4px of type inside
     6px paddings and 1px borders is a 26.4px cap, 2.8px over.
     `margin-block: -0.1875rem` takes 6px back off the margin box, bringing it
     to 20.4px and comfortably inside the line box, while the cap still PAINTS
     at 26.4px. A key in a sentence gets a generous cap and costs no leading.
     The two declarations have to move together: the margin is half the
     padding, negated.

     The 1.4px per side that the cap overhangs the line box lands in prose's
     half-leading rather than on the glyphs above or below, which is where the
     inline chip's own padding goes too. Codex needs none of this because its
     `h-6` cap is drawn standing on its own rather than set in a sentence.

     The paddings are flat, not --space-*: a component's insides do not ride
     the fluid scale. Codex's are 4px vertical (implied by `h-6`) and 6px from
     `px-1.5`; 6px against 8px here is what makes a single key near square at
     the smaller type -- one Space Mono glyph is about 7.5px wide, so `f` draws
     25.5x26.4 and a word like `ctrl` simply gets longer. That leaves the 1.5em
     minimum rarely binding, and it is kept as a floor rather than removed.
     --radius-sm is the house value for Codex's `rounded-sm`, which the dump
     does not resolve. */
  kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 1.5em;
    margin-block: -0.1875rem;
    padding-block: 0.375rem;
    padding-inline: 0.5rem;
    border: var(--border-width-regular) solid var(--neutral-border);
    border-radius: var(--radius-sm);
    background-color: var(--neutral-bg-soft);
    box-shadow: 0 var(--shadow-y-offset) var(--shadow-blur) 0
      var(--shadow-regular);
    color: var(--neutral-text);
    font-size: calc(1em / 1.125 / 1.125);
    line-height: 1;
    white-space: nowrap;
  }

  /* ── Inline code ────────────────────────────────────────────────────────── */

  /* INLINE CODE HAD NO TREATMENT AT ALL BEFORE THIS. css-reset.css gives every
     `code` the mono family and --font-regular, and that was the whole of it --
     a backticked term in a sentence was monospace text on the bare page with
     no chip around it. The code BLOCK has been styled for a while; this is its
     inline counterpart, from Codex's `.inline-code`.

     `:not(pre) > code` is what makes it inline-only. Every code block in this
     theme is `pre > code`, whether Zendesk emitted it or code-block-copy.js
     wrapped it, so excluding a `pre` parent excludes all of them while still
     catching inline code wherever authors put it -- a paragraph, a list item,
     a table cell, a heading, or nested inside `strong`. (0,1,2) here, so it
     also beats the reset's `td pre, td code` pin at (0,0,2) and a term in a
     table cell matches the one in the paragraph above it.

     FOUR HUES WERE PUT ON THE PREVIEW AND INDIGO WON, 2026-10-29. Magenta and
     blue were ruled out on the measurements below; --success-text was tried
     last, on Joy's ask, and rejected on sight. Worth recording that it was
     also the only candidate that failed to reach AAA in both tiers -- 5.89:1
     light and 12.47:1 dark, so AA in light against a 7:1 target -- and that
     the shortfall was the token's and not the hue's: --color-lime-13-5 /
     --color-lime-11 is the same green family at 8.42:1 / 7.82:1 and clears AAA
     in both. If a green is ever wanted here, that is the pair to reach for.

     INDIGO INK, Joy's call on 2026-10-29 to try a hue here rather than the
     inherited prose ink, and indigo out of the three she named because of what
     --brand-text means in this container. Article prose links are
     `color: inherit` at rest and take --brand-text on :hover and :active, so a
     warm red in an article body already reads as "link under the pointer".
     Magenta is the closest of the three to it -- --color-ruby-12 is only ΔE00
     17.6 light / 14.5 dark from --brand-text -- and would have put a resting
     chip next door to a live interaction cue. Indigo clears it at 38.9 / 30.8
     while staying plainly coloured against the prose it sits in (17.3 / 13.7).
     Blue was the third option and is the furthest from the brand red of all
     (45.5 / 45.8), but at ΔE00 14.4 from the prose ink it is the quietest, and
     --color-deep-teal is the same hue family as this fill and the page behind
     it, so it reads as a tint of the background rather than as a decision.

     Both steps are AAA on this fill: --color-indigo-11 is 7.85:1 light and
     --color-indigo-3 is 7.18:1 dark. The ramps are primitives and are NOT
     redefined per tier -- only semantic tokens are -- so the dark value has to
     be written out, which is what the nested `:root[data-theme='dark'] &` is
     for. There is no semantic ink to reach for instead: inline code is not a
     status, so --info-text and its neighbours are the wrong names even where
     the value would pass. Swapping hues is a two-line change to the two
     --inline-code-ink declarations; magenta is --color-ruby-12 / --color-ruby-3
     (7.63:1 / 8.30:1) and blue is --color-deep-teal-11 / --color-deep-teal-5
     (7.25:1 / 7.92:1), both AAA in both tiers as well.

     A LINK'S OWN COLOUR WINS INSIDE IT, which is the rule below and is what
     keeps the hover cue working for `<a><code>…</code></a>` -- an ordinary
     thing to write in an API knowledge base. Without it the chip would hold
     indigo while the rest of the link turned red, and the one element the
     pointer is actually over would be the one that did not respond.

     TWO STEPS DOWN, NOT ONE, Joy's call from the preview -- the chip at one
     step was still reading as large as the prose around it. In body text
     `calc(1em / 1.125 / 1.125)` resolves to --step--1 exactly at both ends of
     the clamp, so inline code now sits on the same step as the code block, and
     this is the second time Space Mono's width and x-height have pulled the
     answer a step smaller than the arithmetic suggested. --step--1 is the
     floor of the scale and this lands ON it rather than below it. Relative
     rather than pinned for the same reason as the keycap: a term inside an h2
     scales with the heading instead of shrinking to body size.

     --neutral-bg-soft is the code block's own surface, so inline and block
     code sit on one fill. Its separation from the page is slight -- ΔE00 3.77
     light / 2.97 dark, and 1.32 in the light corners where `main`'s gradients
     darken -- and --neutral-bg-component would read better in the light middle
     at 5.54. It is not worth it: --neutral-bg-component drops the hovered-link
     ink to AA (6.71:1 / 6.44:1), it is the component fill rather than a
     surface, and it would put inline code on a different plane from the block.
     The border carries the shape here, as it does on the keycap above.

     --neutral-border-soft, DELIBERATELY QUIETER THAN THE KEYCAP'S, which is
     Codex's own distinction between the two and holds up in our tokens: ΔE00
     9.71 light / 12.80 dark against the page middle, against --neutral-border's
     17.01 / 19.95. A keycap is an object and wants a hard rim; inline code can
     appear six times in one paragraph, and at the keycap's weight that
     paragraph reads as peppered with boxes. No shadow, for the same reason.

     THE PADDING IS 1px / 3px, TIGHTENED FROM 2px / 4px ON JOY'S CALL,
     2026-10-29, AND AN INLINE BOX'S PADDING DOES NOT GROW ITS LINE BOX.
     Unlike the keycap, this is a plain inline element: its painted box is the
     content area plus padding and border, while the line box is set by
     line-height alone. So vertical padding here never pushes the lines apart,
     it only grows the chip toward its neighbours -- the 2px it started at put
     the chip at about 22.5px inside a 23.6px line box, close enough to the
     line above that the chips read as crowding it, and 1px pulls that back to
     about 20.5px with roughly 1.5px of clearance per side. The horizontal 3px
     is Codex's own .2rem to the nearest pixel, so tightening landed on the
     dump rather than away from it. Both paddings are flat for the usual
     reason: a component's insides do not ride the fluid --space-* scale. The
     0.25rem radius is Codex's hardcoded 4px; the keycap takes --radius-sm
     instead because Codex's `kbd` uses its radius scale where `.inline-code`
     does not.

     `overflow-wrap: break-word` OVERRIDES THE RESET'S `word-wrap: normal`, and
     the override is the right way round for an inline chip. Keeping code from
     breaking at arbitrary points is correct for a block, which scrolls; inline
     code has no scroller, so a long unbroken token -- an API path, a key --
     would run out of the container on a narrow viewport instead of wrapping.
     This renders properly only because the reset already sets
     `box-decoration-break: clone` on code, which draws the padding and border
     on both fragments when a chip breaks across two lines. */
  :not(pre) > code {
    --inline-code-ink: var(--color-indigo-11);

    padding-block: 0.0625rem;
    padding-inline: 0.1875rem;
    border: var(--border-width-regular) solid var(--neutral-border-soft);
    border-radius: 0.25rem;
    background-color: var(--neutral-bg-soft);
    color: var(--inline-code-ink);
    font-size: calc(1em / 1.125 / 1.125);
    overflow-wrap: break-word;

    /* The ramps are primitives and are declared once, not per tier, so the
       dark value has to be written out. A semantic token would carry its own
       tier split and this block would be a no-op. */
    :root[data-theme='dark'] & {
      --inline-code-ink: var(--color-indigo-3);
    }
  }

  /* THE CARVE-OUT THE INK ABOVE NEEDS. (0,2,2) against the chip's (0,1,2), so
     it wins outright rather than on source order, and it mirrors the
     `a:not([class])` shape this file already uses for prose links -- the ones
     that are `color: inherit` at rest and --brand-text on :hover and :active.
     `color: inherit` is doing real work here, unlike the no-op it would have
     been on the chip itself: it is overriding --inline-code-ink so the chip
     tracks the link through both states.

     Narrow and deliberately so: a CLASSED link wrapping inline code keeps the
     indigo, because a classed link in an article body is a callout link and
     the callout decides its own colours. If one of those turns up looking
     wrong, this selector is where to widen it. */
  a:not([class]) code {
    color: inherit;
  }
}

/**********************/

/* Example article custom styles */
.example pre {
  display: block;
  border: none;
}

/* Icons */
.mod-ui {
  --height: 1.25rem;
  height: var(--height);
  padding-inline: 0.5rem;
}

.mod-store {
  width: 14px;
  height: 14px;
  border-radius: 100px;
  padding: 0.55rem;
  color: var(--clr, var(--clr-white));
  background-color: var(--bg-clr, var(--clr-primary-100));
}

.mod-store-light {
  --clr: #10171e;
  --bg-clr: var(--clr-white);
  border: 1px solid #10171e33;
  opacity: 0.6;
}

.mod-store-library {
  --bg-clr: #deebec;
  --clr: #10171e;
  border: 1px solid #10171e;
}

.mod-store-flask {
  --bg-clr: #ef45231f;
  --clr: var(--clr-primary-100);
  border-radius: 6px;
  border: 1px solid #ef452340;
  padding: 0.25rem;
}

.mod-ui-bell {
  --height: 2rem;
}

.fa-layers {
  width: unset;
}

.fa-layers-counter {
  --fa-counter-border-radius: 1.625rem;
  --fa-counter-line-height: 1;
  --fa-top: -40px;
  --fa-right: -10px;
  --fa-counter-scale: 0.6;
  min-width: unset;
  width: 1.15rem;
  height: 1.15rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/*******************************/

/* A SCROLL CONTAINER ON PAPER IS A GUILLOTINE. `max-block-size: 80vh` with a
   non-visible overflow means everything past the first screenful is clipped and
   simply does not print, and the frame added on 2026-10-27 put every article
   table behind exactly that. The cap is lifted and the overflow opened so
   nothing clips at the viewport height.

   The figures are in the selector list for the horizontal half of the same
   problem, not the vertical one: they carry no height cap (see the fallback
   rule, which deliberately dropped it), but `overflow: auto` on paper still
   clips whatever sticks out sideways, and a table wide enough to need the
   scroller is the one case where that loses content.

   WHY THIS IS A SECOND `@media print` BLOCK and not four more lines in the one
   at the top of the file. Both sets of declarations are written INSIDE
   `.article-content`, so they resolve at (0,2,0) and (0,2,1). A print override
   written as a bare `.article-table-scroll` is (0,1,0) and loses outright, and
   matching the specificity is not enough either, because the first print block
   sits ABOVE the rules it would be fighting -- a tie goes to the later rule.
   Repeating the `.article-content` scope here and placing the block last is
   what makes these win, and it needs no `!important` to do it. The existing
   print block's own rules are unaffected: everything in it targets selectors
   declared at the top level, where (0,1,0) and source order already work.

   The shadow goes for the same reason `.article-container` drops its own on
   paper -- it only shows with "print backgrounds" switched on, and then as grey
   fuzz on A4. THE 1px BORDER DELIBERATELY STAYS, which is where this parts
   company with the card rule: the card's border is decoration around prose,
   while this one is the edge of a data grid, and the `break-inside: avoid` in
   the first print block says tables are meant to read as one unit on paper. */
@media print {
  .article-content .article-table-scroll,
  .article-content figure.wysiwyg-table,
  .article-content figure.wysiwyg-table-align-left,
  .article-content figure.wysiwyg-table-align-right {
    overflow: visible;
    max-block-size: none;
    box-shadow: none;
  }

  /* The sticky header is unpinned for paper. There is nothing to scroll in a
     printed document, so at best it does nothing -- but a sticky box whose
     scrollport has just been given `overflow: visible` above is resolved
     against the page box instead, and that is how a pinned header ends up
     printed over the middle of a table that broke across two sheets. Static
     also hands the header back to `thead { display: table-header-group }`, the
     UA rule that repeats it at the top of each page, which is the behaviour
     that was wanted on paper all along.

     The selector mirrors the shape of the rule it overrides rather than being
     written as plainly as it could be, and that is on purpose. The sticky
     declaration resolves at (0,3,2) -- `.article-content`, then the `:is()`
     group whose most specific argument is `figure.wysiwyg-table`, then
     `th[scope='col']`. A bare `.article-content th[scope='col']` is (0,2,1) and
     would lose outright despite coming later in the file. Repeating the `:is()`
     brings this to the same (0,3,2), where source order decides it. The `table`
     argument is what actually matches -- every `th` is inside one -- and the
     figure argument is carrying the specificity. */
  .article-content :is(figure.wysiwyg-table, table) th[scope='col'] {
    position: static;
  }
}
