/* =============================================================================
   platform.css -- the authenticated layer of the scientific broadsheet
   =============================================================================

   WHAT THIS FILE IS

   The design bible chapter 13, built. It is loaded AFTER static/catalog.css and
   only on the pages served by templates/base.html. It is not a second design
   system: it defines no custom property that catalog.css already defines, it
   consumes the broadsheet's tokens, and it adds only what an authenticated,
   form-bearing, state-mutating surface needs and a read-only anonymous catalog
   does not.

   WHY IT IS A SEPARATE FILE (DEC-20260808T1149Z)

   Eleven public broadsheet pages have no visual gate at all: /tape, /issuers,
   /insiders, /license, the search and disambiguation pages, and the /filing/*,
   /issuer/*, /insider/* and /filings/* families are absent from
   tools/ux/screenshot.py. Editing catalog.css therefore risks regressing eleven
   live anonymous pages with nothing to catch it. Not editing it makes that risk
   zero. tests/test_catalog_routes.py:899 also pins the exact catalog.css link
   tag on sixteen pages, so a cache-buster has to live on a file nothing pins,
   which is this one.

   THE TOKEN COLLISION IS GONE RATHER THAN RESOLVED. styles.css and catalog.css
   share eleven property names, seven with different values, and --t-base is
   220ms in one and 17px in the other. That only ever mattered if both sheets
   loaded on one page. Every page now loads either styles.css or
   catalog.css + platform.css and never both, so --t-base has exactly one
   meaning wherever it is read.

   THE DESTINATION. This file is written so it can be appended to catalog.css
   unchanged, with no renaming, once the catalog pages have a visual gate. That
   is the right end state and it is not the safe move during a grant window.
   ============================================================================= */


/* -----------------------------------------------------------------------------
   0. THE RULE THAT HAS TO COME FIRST
   -----------------------------------------------------------------------------
   Bible 13.0. catalog.css has no [hidden] reset. styles.css:106 does, and four
   live templates depend on it: dashboard.html:43-44, upgrade.html:204, and
   admin_overrides.html:92,136, the last of which says so in a comment.

   The UA rule for [hidden] is `display: none` at the lowest possible
   precedence, so ANY class that sets display beats it. Under the broadsheet
   .stack is `display: grid` and .btn is `inline-flex`. Without this line every
   hidden-until-scripted control on the authenticated pages becomes permanently
   visible: no test fails, no server errors, and the first person to see it is
   whoever opens the deployed site. Measured in WRK-20260808T1132Z section 1.

   It is !important on purpose. The attribute is the platform's own way of
   saying "not now" and it must outrank every component in this file.
   -------------------------------------------------------------------------- */
[hidden] { display: none !important; }


/* -----------------------------------------------------------------------------
   1. THE PAGE FRAME
   -----------------------------------------------------------------------------
   catalog.css lays its pages out from the catalog shell. base.html has its own
   frame vocabulary, kept here so the fifteen migrating templates do not all
   need rewriting at once. .page is the text track, .wide breaks out to the
   flanking track, .full spans the viewport.
   -------------------------------------------------------------------------- */
/* The page track is measure PLUS BOTH FLANKS, matching the `wide` span of the
   catalog's .sheet grid (catalog.css:501-510), which runs
   flank | measure | flank. Sizing it at measure + one flank was measured wrong
   at 1440: the track came out 832px, the header could not fit its nameplate,
   nav, theme setter and sign-in control on one line, and the chrome wrapped to
   two rows while the catalog's header on the same viewport did not. Reading
   text still sets at --measure, because .prose and .lede carry their own
   max-inline-size; this only governs the shell and the wide bands. */
.page {
  inline-size: min(100% - (2 * var(--gutter)), calc(var(--measure) + (2 * var(--flank))));
  margin-inline: auto;
}
.page > .wide { inline-size: 100%; }
.page > .full {
  inline-size: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* A DOCUMENT page, opted into per template. The reading column is centred on
   the page instead of hugging the left flank.

   The frame above is the WIDE track, measure plus both flanks, which is correct
   for the shell and for a page whose content genuinely spans. It is wrong for a
   document. Because .page is a block rather than the catalog's named-line grid,
   `max-inline-size: var(--measure)` on .prose p resolves against 1088px and
   left-aligns, so a prose page set its column at the far left of the sheet and
   left 512px of blank paper down its right side. Measured at 1440px: the
   catalog's own /license put every reading element at left 432, width 576; the
   platform's /terms, /disclaimer, /inside and /transparency put theirs at left
   176, width 576. Same measure, wrong position, and it is the reason a document
   page read as an ordinary web page beside a catalog one (BUG-20260808T2305Z).

   Opt-in rather than a change to .page, because .page also frames the header,
   the footer, the landing page and the dashboard, and those genuinely want the
   wide track. .wide and .full still break out: the :not() pair keeps their own
   sizing, and .page > .full's margin-inline at (0,2,0) already outranks the
   margin here in any case. */
.page--doc > *:not(.wide):not(.full) {
  max-inline-size: var(--measure);
  margin-inline: auto;
}

/* A vertical rhythm primitive. Grid rather than margins so the gap is one
   declaration and no child owns its own outer spacing, which is what makes
   these composable at all. */
.stack { display: grid; gap: var(--sp-5); align-content: start; }
.stack--sm { gap: var(--sp-2); }
.stack--lg { gap: var(--sp-7); }

/* A band is a horizontal division of the page, opened by a rule the way a
   broadsheet opens a section. --band is the catalog's own section spacing, so
   the platform and the catalog breathe identically. */
.band { padding-block: var(--band); border-block-start: var(--rule-heavy); }
.band--tight { padding-block: calc(var(--band) * 0.6); }
/* A BAND THAT OPENS A PAGE PAYS LESS BELOW IT. The gap between two SECTIONS is
   the document rhythm and is right. The gap between a page's title block and
   its first section is that same 62px doing a different job, and on a working
   surface with three short bands it reads as a hole. The PI, on the dashboard:
   "lot of whitespace between 00 and 01" (2026-08-19).

   THE NUMERAL IS NOT THE PROBLEM AND IS NOT TOUCHED. I removed it first and he
   corrected that in one line: it is part of the design language. This is the
   fix he actually asked for, and it is the same distinction the standfirst cap
   taught earlier today, where I shortened three sentences instead of removing
   the rule that was breaking them.

   DECLARED HERE AND NOT IN catalog.css, which is where I put it first and
   watched it lose. `.band` and `.band--tight` are declared in BOTH sheets, this
   one loads second, and the comment ten lines below already says so. A rule
   that has to beat platform.css must live in platform.css. That is
   DEC-20260808T1149Z's standing cost for the third time today, after the ticker
   chip and the suggestion list.

   Only the bottom moves: the space ABOVE the title still separates it from the
   masthead, which is a different relationship and reads correctly. */
.band--opens { padding-block-end: calc(var(--band) * 0.3); }
.band:first-child { border-block-start: 0; }
/* A BAND THAT OPENS WITH A SECTION HEAD DOES NOT DRAW ITS OWN RULE, because
   the section head already drew one (BUG-20260810T0657Z).

   `.band` is declared in BOTH sheets and this one loads second, so the two
   disagree about what a band is: catalog.css gives it no rule of its own and
   seams consecutive bands with a hairline, leaving `.section-head` to open each
   section, while this file opened every band with a heavy rule. The composition
   every catalog page uses, `section.band > div.section-head`, therefore drew
   two identical 3px ink rules 65px apart on a platform page. Measured on both
   shells 2026-08-10.

   It had never been seen because the four platform templates that compose that
   way all have exactly one band, and `.band:first-child` above zeroes it. The
   trap fires on a page's SECOND band, which is what the dashboard's composition
   has (DEC-20260810T0652Z).

   This makes the platform agree with the catalog for this composition and
   changes nothing that renders today: every current `.band > .section-head` is
   already a first child. It is not the full reconciliation of the two
   declarations, which is still owed and is still its own decision; it closes
   the one case that is about to have callers. */
.band:has(> .section-head) { border-block-start: 0; }
/* A BAND'S HEADING IS SUBORDINATE TO THE PAGE'S, and on this platform that is
   a measured convention rather than a preference. `.section-head h2` is
   `--t-head`, the same 38px an h1 takes, because on a catalog entity page the
   band heads ARE the loudest thing and the page's own name lives in `.entity`.
   A platform page has its h1 in a `.section-head` instead: measured on
   /terms, /disclaimer and /inside, the h1 renders at 38px and every body h2 at
   30px.

   A composition with a numbered page head AND numbered bands therefore rendered
   three headings at 38px, which is the flatness DEC-20260810T0652Z exists to
   fix arriving by a different route. Only the page head keeps `--t-head`. */
.band .section-head h2 { font-size: var(--t-sub); }

.card-row {
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

/* Prose helpers. .lede is the standfirst under an h1; .hint and .muted are the
   two de-emphasis levels, and they are two rather than one because a hint is
   guidance the reader wants and muted is furniture the reader does not. */
.lede { font-size: var(--t-sub); line-height: var(--lh-lead); color: var(--c-ink-strong); max-inline-size: var(--measure); }
/* .hint is DOUBLED on purpose, as are .t-sm, .t-2xs and .t-md below: a size
   utility is an author's explicit per-element decision and must outrank any
   container's descendant default. At a single class, (0,1,0), every one of
   them lost to catalog.css's `.card p` at (0,1,1), and all seventeen card
   paragraphs on the landing page rendered 17px whatever the template wrote:
   five declared steps, 11px to 19px, flattened to one. The same defect shipped
   and was "fixed" once before by template surgery on one page
   (BUG-20260810T0535Z), which is why it came back: the rule was never fixed,
   only the instance. Doubling the class is (0,2,0), which beats any
   one-container-one-element default while staying beneath every two-container
   rule. WRK-20260819T1401Z. */
.hint.hint { font-size: var(--t-mini); color: var(--c-ink-muted); max-inline-size: var(--measure); }
.muted.muted { color: var(--c-ink-muted); }
/* RUNNING PROSE, NOT FURNITURE (WRK-20260819T1401Z). The store's scope
   statement is sentences a reader is asked to believe, and it rendered as the
   system's ONLY 13px ui-face passage, at 164 characters a line across 1088px,
   while section 4's face table says the ui face is "furniture only... Never
   prose". Text face, a reading size, the muted ink every other coverage line
   already uses, and the measure. */
.coverage {
  font-family: var(--face-text);
  font-size: var(--t-small);
  color: var(--c-ink-muted);
  max-inline-size: var(--measure);
}

/* ROW gap larger than COLUMN gap, and the asymmetry is measured rather than
   aesthetic. `.btn--sm` meets the 44px floor with an invisible hit extension,
   so a 31.2px button grows 6.4px above and below. Two of them stacked need
   12.8px between them, and a uniform `--sp-3` gives 12px, so their extensions
   overlapped by 0.8px and bible 13.8's law that two targets may never contest
   one point was broken. Measured on /dashboard at 390: "Email me about all"
   hit-tested at 43px against the 44 floor while its neighbour, on top at the
   seam, measured fine.

   `--sp-4` is 16px and clears it. The column gap is untouched, because side by
   side the buttons never collide: the extension is inset to nothing on an axis
   the drawn box already satisfies, and these are wider than 44px. */
.btn-group { display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-3); align-items: center; }

/* The accessible-hiding primitive. clip-path rather than clip, and NOT
   display:none, because a screen reader must still reach it. */
.visually-hidden {
  position: absolute; inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; inset-block-start: 0; inset-inline-start: 0;
  transform: translateY(-100%);
  z-index: 10;
  padding: var(--sp-3) var(--sp-5);
  background: var(--c-mark); color: var(--c-on-mark);
  font-family: var(--face-ui); font-size: var(--t-small);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
}
.skip-link:focus-visible { transform: translateY(0); }


/* -----------------------------------------------------------------------------
   2. THE AUTHENTICATED SHELL
   -----------------------------------------------------------------------------
   Bible 13.1. The catalog nameplate says what the publication is. The platform
   nameplate additionally says who is reading, as a byline rather than an
   avatar, in the same slot the catalog uses for its
   "PUBLIC · NO ACCOUNT · MACHINE-READABLE" line. The two shells therefore have
   the same silhouette and differ by one line of type.

   THE SESSION PREDICATE IS SINGLE-SOURCED. base.html computes _signed_in from
   _user.get("id") and nothing else may re-derive it. BUG-20260729T0049Z was a
   signed-in shell rendered to anonymous visitors, and nothing in this file may
   make .byline--account or .account visible outside that branch.
   -------------------------------------------------------------------------- */
/* EVERY CLASS NAME BELOW IS THE ONE ALREADY IN templates/base.html, and none of
   them is renamed. Two reasons, and the second is the binding one:

   1. base.html's own script queries `.account-menu` directly to close the
      disclosure on outside click. A rename breaks that with no test failure,
      no server error, and nothing visibly wrong until someone tries to dismiss
      the menu (WRK-20260808T1132Z section 4).
   2. Restyling should not need a markup edit. The shell already uses a
      <details> disclosure, which is exactly what bible 13.1 specifies, so the
      existing markup was right and only its clothes were wrong. */

.site-header { border-block-end: var(--rule-nameplate); }
.site-header__inner {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3) var(--sp-5);
  padding-block: var(--sp-4);
}
/* The nameplate. The mark keeps its diamond, which is the same rotated square
   the system uses for "this one" on a radio and on an interval's point
   estimate, so the brand mark and the data vocabulary agree. */
.logo {
  display: inline-flex; align-items: baseline; gap: var(--sp-2);
  font-family: var(--face-text);
  /* ONE LOGO, EVERYWHERE. --t-sub at every width on both shells, by the PI's
     instruction of 2026-08-19: "the diamond size keeps changing across the
     pages. We must have ONE uniform Logo." Both shells' marks are 0.62em of
     their wordmark, so one wordmark token is what makes one diamond. The
     platform desktop wore --t-title (50px, a 31px diamond) against the
     catalog's 32px and a 20px diamond; the platform steps down to match,
     because the catalog's compact nameplate is the one that was designed as
     a bar. WRK-20260819T1401Z. */
  font-size: var(--t-sub); font-weight: var(--w-bold);
  letter-spacing: var(--tr-nameplate); line-height: var(--lh-solid);
  color: var(--c-ink-strong); text-decoration: none;
  margin-inline-end: auto;
  /* The 44px floor in the box, stated, not rented. Before this the home
     link's target height was whatever the type size happened to buy, which
     is why the phone wordmark wore an untokenised 2.125rem for nine days:
     34px type bought a 46px line box and swapping in the token dropped the
     target to 41px, so the literal was load-bearing by accident. With the
     floor declared, the size below can be a token like everything else.
     WRK-20260819T1401Z. */
  min-block-size: 2.75rem;
}
/* CENTRED ON THE WORDMARK, NOT SAT ON ITS BASELINE, and set relative to it
   rather than pinned. The catalog shell already does both in
   `.nameplate__mark`, and the PI's instruction on 2026-08-18 was to make this
   shell match that one, which is the right direction: the catalog's is correct
   and this one is the odd shell out.

   Two differences, and only the second was visible. `--t-sub` pinned the mark
   at 30px inside a 48px wordmark, which happens to be 0.625em and so matches
   the catalog's 0.62em by coincidence rather than by rule; a change to either
   nameplate size would have broken it silently. And `.logo` is a flex row with
   `align-items: baseline`, so a glyph whose ink sits centred in its em box was
   aligned by its baseline and dropped below the wordmark's optical centre.
   Measured on production at 1440: mark centre 47px against the wordmark's 40px,
   seven pixels low.

   `align-self: center` on the mark alone rather than `align-items: center` on
   `.logo`, because the wordmark and anything else in that row still want the
   baseline they have always had. This is the fourth cross-shell defect the PI
   has found by eye and the third in the wordmark (BUG-20260810T0602Z,
   BUG-20260817T1432Z), which is the argument for WRK-20260810T0603Z's
   cross-shell lens rather than for another one-off. */
.logo__mark {
  font-size: 0.62em;
  align-self: center;
  color: var(--c-ink);
}

/* Who is reading, set as a byline under the nameplate rather than as an avatar,
   in the slot the catalog uses for PUBLIC · NO ACCOUNT · MACHINE-READABLE. */
.byline--account {
  font-family: var(--face-ui);
  font-size: var(--t-micro); letter-spacing: var(--tr-caps);
  text-transform: uppercase; color: var(--c-ink-muted);
}

.primary-nav ul { display: flex; flex-wrap: wrap; gap: var(--sp-5); list-style: none; margin: 0; padding: 0; }
/* THE PAGE'S OPENING CLAIM IS NOT ITS SMALLEST TYPE.
   "OPEN · PEER REVIEWED · FREE FOR ACADEMIA" is the first thing the front page
   says and it was set at --t-micro, 11px, the size this system reserves for
   LABELS: smaller than every other word on the page including the provenance
   stamp. The PI, of the phone screenshot: "The academic tier is the tiniest
   font size too."

   He is right on the merits and on the law. Section 3's ladder makes --t-micro
   a label size, and a standing claim about what the product IS is not a label,
   it is the deck's sibling. --t-mini keeps the strip thin and stops it being
   the quietest thing on a page it opens.

   The `.kicker` label size elsewhere is untouched: where the class genuinely
   labels a section it is doing its job. */
.hero .kicker--plain { font-size: var(--t-mini); }

/* TWO ROWS ON A PHONE, NOT FOUR.
   Measured at 390: four rows and 266px of masthead, 32% of the screen spent
   before a word of the page, and the wordmark sat alone on a row of its own
   because it and the controls could not fit one line at 48px. The PI sent a
   photograph of it from his phone and I let it sit for hours.

   Wordmark and controls share the first line; the nav takes the second. That
   is the ordinary phone masthead. (An earlier version of this comment claimed
   it is "what the catalog shell already does"; it is not: the catalog
   masthead deliberately STACKS below 64rem, because squeezing it would cost
   the 44px floor on every control, and that decision is recorded there. The
   two shells solve the same width differently for measured reasons, and a
   comment asserting parity that does not exist is how two audit findings
   burned their first hour. WRK-20260819T1401Z.)

   The wordmark steps down from --t-title to --t-sub, the token the catalog's
   compact nameplate reads, because "a reader arrives on a page, not on a
   cover". (This sentence used to say 34px "the size the catalog's own compact
   nameplate has used all along": the catalog measured 30px on the day that
   was written, so the claim was false at birth. One token in both shells now
   makes the claim true by construction rather than by assertion.)

   The nav is NOT hidden behind a hamburger. Three words are not a menu, and a
   disclosure would cost a tap, a script dependency and a focus trap to conceal
   them, which is the argument the primary nav's own comment already makes. */
@media (max-width: 48rem) {
  .site-header__inner {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--sp-2) var(--sp-4);
  }
  /* Declared AFTER `.logo` rather than before it: both are one class, so at
     equal specificity source order decides, and the first version of this block
     sat above `.logo` and silently lost its font-size. Measured, not assumed:
     the wordmark stayed 48px and the audit said so.

     --t-sub, the same token the catalog's compact nameplate reads, so the two
     shells' phone wordmarks are one value by CONSTRUCTION. The 2.125rem that
     stood here carried a comment claiming it was "the size the catalog's own
     compact nameplate has used all along"; the catalog's measured 30px on the
     day that was written, so the claim was false at birth, and the two shells
     signed themselves 34px against 30px until the audit caught it. The 44px
     target this literal was accidentally buying is now bought honestly by
     .logo's own min-block-size. */
  .site-header__brand { grid-column: 1; grid-row: 1; font-size: var(--t-sub); }
  .site-header__inner > .row { grid-column: 2; grid-row: 1; justify-self: end; }
  .primary-nav { grid-column: 1 / -1; grid-row: 2; }
  /* ONE LINE THAT NEVER WRAPS, superseding the two-even-column grid that
     stood here. The grid was itself a fix for a ragged 2+1 wrap the PI
     photographed, and it traded the rag for a different spillage: an
     orphaned third item under an empty cell, 44px rows stacked 40px apart,
     which he then photographed too ("the three menu items look like someone
     spewed them there"). The lesson is that BOTH failures are the same
     failure: a nav that wraps at all reads as scattered on a phone.

     So it takes the strip grammar the catalog folio uses on the same widths:
     the folio's own type step, middway tracking, one 44px line, and when a
     width cannot hold it the strip SCROLLS, the tabs convention every phone
     user knows, with the half-visible item at the edge as its own
     affordance. At the folio step the three items fit 390 outright; the
     scroll is for narrower phones. One grammar, two shells.
     WRK-20260819T1401Z. */
  .primary-nav ul {
    display: flex; flex-wrap: nowrap;
    gap: var(--sp-5);
    overflow-x: auto;
    scrollbar-width: none;
    /* The strip is a drawn row of the bar, not text floating between the
       wordmark and the first band: a hairline above ties it to the identity
       row exactly as the catalog strip is tied to its nameplate. */
    border-block-start: var(--rule-hair);
    padding-block: var(--sp-1);
  }
  .primary-nav ul::-webkit-scrollbar { display: none; }
  .primary-nav li { flex: none; }
  .primary-nav .nav-link { white-space: nowrap; font-size: var(--t-micro); }
}

/* The 2.75rem floor is carried in the BOX rather than as a ::before extension,
   because these sit in a wrapping flex row and an invisible halo on a 33px box
   would overlap its neighbour above and below: two targets contesting one
   point, which section 8 forbids outright. The header does not grow, because
   the logo beside them is already 48px (measured: 86px header, unchanged).
   Before this rule the drawn box was 33px and the real hit target, measured
   with elementFromPoint rather than getBoundingClientRect, was 34x81
   (BUG-20260808T2330Z). */
.nav-link {
  display: inline-flex; align-items: center;
  min-block-size: 2.75rem;
  font-family: var(--face-ui); font-size: var(--t-mini);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  color: var(--c-ink); text-decoration: none;
  border-block-end: 2px solid transparent;
}
.nav-link:hover { border-block-end-color: var(--c-rule); }
/* aria-current is the source of truth for "you are here", so the style keys to
   the attribute rather than to a class a template has to remember to add. It is
   an underline rather than a colour change, so it survives greyscale (law 2). */
.nav-link[aria-current="page"] { border-block-end-color: var(--c-ink); color: var(--c-ink-strong); }

.row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.row--tight { gap: var(--sp-2); }

/* Three real buttons in a ruled set, the same construction as the catalog's
   PAPER / AUTO / INK setter. The labels differ and the stored value does not:
   DEC-20260808T1149Z makes both shells write `ebomi-theme`. */
/* The header's controls are a ruled set and must share one height. The theme
   setter is 46px, a 44px button inside a 1px rule on each side, and .btn--sm
   drew at 31px, so the two sat centred on one line with visibly different top
   and bottom edges: misaligned to the eye even though the centres agreed.
   catalog.css:2022 sets the same precedent for .entity__kicker, and the reason
   is the same: a small button is small on purpose EXCEPT beside a full-height
   control (BUG-20260810T0012Z).

   `stretch` on the cluster rather than a height on the button, because the
   number that has to match is the setter's OUTER height and no rule can name
   it: 44px of button plus 2px of rule. Stretch makes the row's own height the
   contract, so the two edges line up whatever the setter's border becomes. */
.site-header .row { align-items: stretch; }
.site-header .btn--sm { min-block-size: 2.75rem; }
.site-header .btn--sm::before { content: none; }
/* The bar's controls are chrome beside the brand, never rivals of it: the PI,
   of the signed-out header, "that sign in rectangle is larger than the logo.
   Bad." Exactly the floor, a quiet label size, padding that fits inside the
   44px box. Applies to any .btn the bar carries, so the next control cannot
   re-grow past the wordmark either. */
.site-header .btn {
  min-block-size: 2.75rem;
  padding: 0 var(--sp-4);
  font-size: var(--t-mini);
}

/* THE THEME CONTROL IS A MENU, NOT A SEGMENTED BAR.
   The PI sent Render's own picker as the model: a small trigger, and behind it
   a vertical list with an icon per row and a tick on the one in force. Three
   ruled segments sitting permanently in the masthead were the loudest element
   in it, which is law 1 inverted, and he had said so twice before sending a
   picture.

   The list is achromatic. The current row is marked by a TICK and by ink
   weight, never by a fill: law 2 says the meaning has to survive greyscale, and
   a tick survives it better than any colour could. The old control marked the
   current segment with `--c-mark`, a filled block, which is the vocabulary this
   system reserves for data. */
.theme-menu { display: grid; }
.theme-menu__item {
  display: grid;
  grid-template-columns: 1rem 1fr 0.9rem;
  align-items: center; gap: var(--sp-3);
  inline-size: 100%; min-block-size: 2.5rem;
  padding-inline: var(--sp-4);
  font-family: var(--face-ui); font-size: var(--t-mini);
  text-align: start;
  background: none; border: 0; color: var(--c-ink-muted); cursor: pointer;
}
.theme-menu__item:hover { background: var(--c-hover-wash); color: var(--c-ink-strong); }
.theme-menu__item[aria-pressed="true"] { color: var(--c-ink-strong); font-weight: var(--w-semi); }
.theme-menu__tick { visibility: hidden; }
.theme-menu__item[aria-pressed="true"] .theme-menu__tick { visibility: visible; }
.theme-menu__item svg { display: block; }

/* The trigger. One control at the size of its neighbours, which is the whole
   point: what used to be three boxes is now one. */
.theme-picker { position: relative; }
.theme-picker > summary {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-block-size: 2.75rem; padding-inline: var(--sp-3);
  font-family: var(--face-ui); font-size: var(--t-mini);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  color: var(--c-ink-muted);
  border: var(--rule-control); cursor: pointer; list-style: none;
}
.theme-picker > summary::-webkit-details-marker { display: none; }
.theme-picker > summary::after { content: "\25BE"; font-size: var(--t-micro); }
.theme-picker[open] > summary::after { content: "\25B4"; }
.theme-picker__icon { display: inline-flex; }
/* THE TRIGGER IS A SQUARE THAT REPORTS THE STATE.
   113x44 with the word THEME taking 51 of it, for a control that names all
   three of its options the moment it opens. Law 1: the chrome is subordinate.
   44x44 now, which is the floor and therefore the smallest it is allowed to be.

   All three icons ship and CSS picks the live one, so the trigger tracks the
   state with NO new script. The state already lives in `aria-pressed`, which
   the existing paintToggle() maintains, so this reads the same source the menu
   draws its tick from and cannot disagree with it. */
.theme-picker > summary {
  inline-size: 2.75rem; min-inline-size: 0; padding-inline: 0;
  justify-content: center; gap: 0;
}
.theme-picker > summary::after { content: none; }
.theme-picker__icon { display: none; }
/* The fallback is System, which is also the default, so a trigger rendered
   before any script has run shows the state the page is actually in. */
.theme-picker__icon--system { display: inline-flex; }
.theme-picker:has([data-theme-choice="light"][aria-pressed="true"]) .theme-picker__icon--system,
.theme-picker:has([data-theme-choice="dark"][aria-pressed="true"])  .theme-picker__icon--system { display: none; }
.theme-picker:has([data-theme-choice="light"][aria-pressed="true"]) .theme-picker__icon--light { display: inline-flex; }
.theme-picker:has([data-theme-choice="dark"][aria-pressed="true"])  .theme-picker__icon--dark  { display: inline-flex; }
/* A CLOSED DISCLOSURE MUST NOT PAINT ITS PANEL, STATED EXPLICITLY.
   Measured at 390px on the front page: `details.open` was FALSE and the panel
   still computed `display: block` with a real 176x138 box, so the theme menu
   stood permanently under the masthead and the header measured 266px in seven
   rows. The browser's own hiding of a closed `<details>`' content did not reach
   an absolutely positioned child here, which is the sort of thing that differs
   by engine and is not worth relying on either way.

   Written for BOTH panels rather than the one that was caught, for the same
   reason the dismiss handler is bound to `header details` rather than to a
   class: a rule a pattern needs belongs on the pattern. */
.theme-picker:not([open]) .theme-picker__panel,
.account-menu:not([open]) .account-menu__panel,
.folio-account:not([open]) .folio-account__panel { display: none; }

.theme-picker__panel {
  position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + var(--sp-2));
  z-index: 5; min-inline-size: 11rem;
  background: var(--c-surface); border: var(--rule-control);
  padding-block: var(--sp-2);
}

/* The account affordance. Already a <details> disclosure in the markup, which
   is what bible 13.1 argues for: it works with scripting disabled, needs no
   focus trap, and the broadsheet has no vocabulary for a floating card.
   No shadow and no radius (law 7). */
.account-menu { position: relative; }
.account-menu > summary {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-block-size: 2.75rem; padding-inline: var(--sp-3);
  font-family: var(--face-ui); font-size: var(--t-mini);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  border: var(--rule-control); cursor: pointer; list-style: none;
}
.account-menu > summary::-webkit-details-marker { display: none; }
.account-menu > summary::after { content: "\25BE"; font-size: var(--t-micro); }
.account-menu[open] > summary::after { content: "\25B4"; }
.account-menu__panel {
  position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + var(--sp-2));
  z-index: 5; min-inline-size: 14rem;
  background: var(--c-surface); border: var(--rule-control);
  display: grid;
}
/* THE SUMMARY CARRIES AN ADDRESS NOW, not the word "Account", so it can no
   longer be uppercased or allowed to set the header's width. Lowercase because
   an email address is a string to be READ and matched, not a label; truncated
   because a long institutional address would otherwise push the nav off a
   laptop; and the full value is in the title attribute for anyone who needs it.
   PI, 2026-08-19, asking for the Render-shaped cluster. */
.account-menu > summary {
  text-transform: none; letter-spacing: var(--tr-flat);
  max-inline-size: 16rem; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.account-menu__meta {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
  padding: var(--sp-3) var(--sp-4) 0;
}
/* The address in full inside the panel, where there is room for it and where a
   reader on a shared machine goes to check who they are. Set in the identifier
   face, because that is what it is. */
.account-menu__email {
  font-family: var(--face-ident); font-size: var(--t-mini);
  color: var(--c-ink-strong);
  padding: 0 var(--sp-4) var(--sp-3);
  overflow-wrap: anywhere;
}
/* THE THEME CONTROL IN THE PANEL IS CHROME AND SITS AT ITS OWN SIZE. My first
   version stretched it to `repeat(3, 1fr)` across the panel, which made three
   large blocks and gave a preference more weight than Sign out directly below
   it. The PI called it "way too large, looks ugly" and asked whether it was in
   line with the bible; it was not (2026-08-19).

   Law 1 is that the chrome is achromatic and subordinate, and the density
   ladder's own review rule is that a gear louder than its surface is a defect
   rather than a taste. A theme preference is the quietest thing in this menu
   and now looks it: intrinsic width, the panel's own type scale, and the 44px
   floor met by the row rather than by three stretched boxes.

   `min-block-size: 0` on the buttons because the row supplies the target: the
   whole control is 2.25rem and each button spans it, which is a hit area of
   36px by its own width. Kept above the floor by the invisible extension the
   rest of this stylesheet uses, rather than by growing the box. */
.account-menu__panel .theme-menu { margin-block-end: var(--sp-2); }
.account-menu__panel hr {
  margin: 0; border: 0; border-block-start: var(--rule-hair);
}
.account-menu__item {
  min-block-size: 2.75rem; display: flex; align-items: center;
  padding: var(--sp-2) var(--sp-4);
  font-family: var(--face-ui); font-size: var(--t-small);
  color: var(--c-ink); text-decoration: none;
  border-block-end: var(--rule-hair);
}
.account-menu__item:last-child { border-block-end: 0; }
.account-menu__item:hover { background: var(--c-hover-wash); }
.account-menu hr { border: 0; border-block-start: var(--rule-thin); margin: 0; }

.site-footer { border-block-start: var(--rule-mid); padding-block: var(--sp-8); color: var(--c-ink-muted); }
.site-footer > .page > * { font-size: var(--t-mini); }
.site-footer ul { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); list-style: none; margin: 0; padding: 0; }
/* Seven links at a drawn height of 17px, which missed section 8's 44px floor
   and also WCAG 2.2 SC 2.5.8's 24px minimum; a footer link in a list does not
   take that criterion's inline-in-a-sentence exception. Real height rather than
   a hit extension for the same reason as .nav-link: these wrap, and a halo
   would have neighbours contesting one point.

   `display: flex` on the LI was wrong and is removed. One footer item is
   "Questions? <a>ping@ebomi.com</a>", so the plain text became an anonymous
   flex item beside the link and the pair stopped reading as a sentence
   (BUG-20260810T0012Z). inline-flex on the anchor alone gives the anchor its
   floor and leaves the text flowing around it. */
.site-footer nav a {
  display: inline-flex; align-items: center;
  min-block-size: 2.75rem;
}
.site-footer a { color: var(--c-ink); }
/* The one sentence in the footer that is news rather than furniture: the
   product is now free. It takes the body size back off the rule above and the
   full ink colour, because a notice nobody can read is not a notice. It is the
   only channel to the 925 registered accounts that never completed a login and
   are deliberately not being emailed. */
.footer-notice {
  font-size: var(--t-small) !important;
  color: var(--c-ink-strong);
  font-weight: var(--w-medium);
}


/* -----------------------------------------------------------------------------
   3. FEEDBACK: PRINTED, NOT FLOATED  (law 11)
   -----------------------------------------------------------------------------
   Bible 13.2. The shipped platform used a toast region: a message that appears
   over the page and removes itself on a timer. It is deleted rather than
   restyled, because a message that removes itself cannot be re-read, cannot be
   quoted and cannot be checked, and this product's entire claim is that what it
   says can be checked. A timer also decides how fast someone reads, which no
   document may do.

   Four states, told apart by kicker word, rule style and mark, NEVER by pigment
   (law 1). NOT DONE and FAULT are separate because they are separate facts: the
   first says the system understood and declined, the second says the system did
   not work.
   -------------------------------------------------------------------------- */
.notice {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  border-block-start: var(--rule-mid);
  background: var(--c-note-wash);
  max-inline-size: var(--measure);
}
.notice__kicker {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
  display: flex; align-items: baseline; gap: var(--sp-2);
}
/* The mark is generated rather than authored so a template cannot ship the
   wrong glyph for the state, and it is aria-hidden by being generated content:
   the kicker word carries the meaning for a screen reader. */
.notice__kicker::before { font-family: var(--face-fig); }
.notice__body { font-size: var(--t-small); line-height: var(--lh-prose); color: var(--c-ink); }

.notice--said    .notice__kicker::before { content: ""; }
.notice--worked  .notice__kicker::before { content: "\2713\00a0"; }   /* ✓ */
.notice--refused .notice__kicker::before { content: "\2716\00a0"; }   /* ✖ */
.notice--faulted .notice__kicker::before { content: "\00b6\00a0"; }   /* ¶ */

/* The at-cap swap offer inside a refused notice: one removable company per
   row, hairline-separated, each row one form whose button names the object
   it removes (bible 13.3). WRK-20260819T1401Z. */
.swap-list { list-style: none; margin: 0; padding: 0; display: grid; }
.swap-list > li { border-block-start: var(--rule-hair); padding-block: var(--sp-2); }
.swap-list > li:first-child { border-block-start: 0; }

/* The closure confirmation row: the typed address, the act, the way out.
   Wraps on a phone; the input keeps a measure so an address is readable. */
.close-confirm { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
.close-confirm .input { flex: 1 1 16rem; max-inline-size: 24rem; }

/* A refusal is doubled rather than pigmented: two rules where the others have
   one, which is a difference of shape and survives greyscale and every
   dichromacy (law 2). */
.notice--refused { border-block-start: var(--rule-mid); box-shadow: inset 0 4px 0 -2px var(--c-canvas), inset 0 6px 0 -2px var(--c-rule-strong); }
/* A fault is dashed, the system's existing meaning for machinery rather than
   decision (law 7). */
.notice--faulted { border-block-start: 2px dashed var(--c-rule-strong); background: transparent; }


/* -----------------------------------------------------------------------------
   4. DESTRUCTIVE ACTIONS, WITH THE OBVIOUS ANSWER FORBIDDEN
   -----------------------------------------------------------------------------
   Bible 13.3. Every convention for a destructive control is a red button, and
   law 1 forbids it: the two pigments carry direction on transactions and
   nothing else in the system may take either. catalog.css has no destructive
   variant, which is the law holding rather than an oversight.

   So the channel is statement, not colour. A destructive action is never a lone
   button: it is a .consequence line stating in words what will be lost, with
   the action inside it, and the label NAMES THE OBJECT. "Remove" is a verb with
   no accusative and is the reason people undo things.
   -------------------------------------------------------------------------- */
.consequence {
  display: grid; gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-start: var(--rule-hair);
  max-inline-size: var(--measure);
}
.consequence__text { font-size: var(--t-small); line-height: var(--lh-prose); color: var(--c-ink); }
.consequence__text strong { font-weight: var(--w-semi); color: var(--c-ink-strong); }

/* The one place a strike-through appears in the whole system, so the shape of
   the control says removal in greyscale and at 390px. */
.btn--struck .btn__label {
  text-decoration: line-through;
  text-decoration-color: var(--c-rule-strong);
  text-decoration-thickness: 2px;
}
.btn--struck:hover .btn__label { text-decoration-color: currentColor; }


/* -----------------------------------------------------------------------------
   5. FORMS THAT MUTATE
   -----------------------------------------------------------------------------
   Bible 13.4. catalog.css draws .field, .input, .select, .check and .fieldset
   for two GET search forms. These are the additions a POST needs.
   -------------------------------------------------------------------------- */

/* A form that has been submitted is disabled AT THE FIELDSET, not at the
   button. Re-entry is the defect, and a disabled button with live inputs still
   submits on Enter. */
.fieldset[disabled], fieldset[disabled] { opacity: 0.6; cursor: not-allowed; }
.fieldset[disabled] .input, fieldset[disabled] .input { border-block-end-style: dotted; }

/* The submit control states the phase and NEVER MOVES. The label text does not
   change to "Saving...", because a label that changes width relayouts the row
   under the reader's cursor. The rule sweeps beneath it instead. */
.btn[data-phase="working"] { cursor: progress; }
.btn[data-phase="working"]::after {
  content: ""; position: absolute;
  inset-inline: var(--sp-3); inset-block-end: 4px; block-size: 2px;
  background: currentColor;
  animation: sweep var(--dur-page) var(--ease-inout) infinite;
}
@keyframes sweep { 0% { transform: scaleX(0); transform-origin: left; } 50% { transform: scaleX(1); transform-origin: left; } 51% { transform-origin: right; } 100% { transform: scaleX(0); transform-origin: right; } }
@media (prefers-reduced-motion: reduce) {
  .btn[data-phase="working"]::after { animation: none; background: none; border-block-start: 2px dashed currentColor; }
}

/* The error summary links to the first invalid field by id. Errors are keyed to
   :user-invalid so an untouched field is never shown as an error, which is
   catalog.css's existing contract and is not restated here. */
.error-summary a { color: var(--c-ink-strong); }


/* -----------------------------------------------------------------------------
   6. THE CONTAINER FAULT STATE, PORTED
   -----------------------------------------------------------------------------
   Bible 13.5. catalog.css ships .blank and does NOT ship .fault, so law 8 is
   only half enforceable on any surface using the shipped sheet. This is the
   reason the landing page tells visitors "The store did not answer" when the
   store answered correctly with zero rows: empty and faulted were one string
   because the system shipped one panel (BUG-20260808T1131Z).

   The fault OUTRANKS the blank. An empty body under a declared fault never
   claims that no insider traded.
   -------------------------------------------------------------------------- */
.fault {
  display: grid; gap: var(--sp-3);
  padding: var(--sp-5);
  border: 2px dashed var(--c-rule-strong);
  max-inline-size: var(--measure);
}
.fault__mark { font-size: var(--t-head); color: var(--c-rule-strong); line-height: var(--lh-solid); }
.fault__title { font-size: var(--t-lead); font-weight: var(--w-semi); color: var(--c-ink-strong); }
.fault__body { font-size: var(--t-small); line-height: var(--lh-prose); max-inline-size: 42ch; }
.fault__stamp {
  font-family: var(--face-fig); font-size: var(--t-mini);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
  color: var(--c-ink-muted);
}
/* Declared by the author on the wrap, because faulted-ness is not knowable from
   the DOM, exactly as data-filtered is for the filtered-empty state. */
[data-fault] .blank { display: none; }


/* -----------------------------------------------------------------------------
   7. PRINT
   -------------------------------------------------------------------------- */
@media print {
  .site-header, .site-footer, .skip-link, .account, .primary-nav { display: none; }
  .notice, .fault { border: 1px solid #000; background: none; }
}


/* -----------------------------------------------------------------------------
   8. THE COMPATIBILITY LAYER
   -----------------------------------------------------------------------------
   THE STRATEGY, AND WHY IT IS THIS ONE.

   The fifteen migrating templates use 182 distinct class names. The obvious
   migration is to rewrite their markup to the broadsheet's vocabulary. That is
   the wrong move here, for a measured reason: four of those names are queried
   by JavaScript (.account-menu, .listbox__option, .combobox, and
   input.cand-display, the last of which has no CSS rule at all so nothing about
   the page warns you it exists), two are asserted literally in CI, and four
   more are built by string interpolation and so cannot be found by grepping for
   the modifier. A rename breaks those silently: no test fails and no server
   errors (WRK-20260808T1132Z section 4).

   So platform.css RE-IMPLEMENTS THE EXISTING NAMES IN THE BROADSHEET'S
   LANGUAGE. Migrating a page becomes a stylesheet swap plus one opt-in line,
   not a markup rewrite, and the silent-breakage class is removed rather than
   managed. The names are ebomi's own history and will read oddly next to the
   catalog's; that is a cost worth paying for five months, and renaming can
   happen later against a green screenshot gate.

   THE SIX FALSE FRIENDS. These names exist in catalog.css with a DIFFERENT
   meaning, so a migrated page inherits the wrong rule unless this file
   overrides it. platform.css loads after catalog.css, so an equal-specificity
   rule here wins.

     ident      catalog has no top-level rule, only .result__meta .ident and
                .entity__kicker .ident, so an unscoped .ident gets nothing
     wide/full  catalog scopes them to `.sheet > .wide`
     card       catalog's is a subgrid row member; ebomi's is a panel
     card-row   catalog hard-codes 4 bands; ebomi's is auto-fit
     hero       catalog's grid lives on .hero__grid, one level down
   -------------------------------------------------------------------------- */

/* .card is a panel here, not a subgrid row member. Used by the on-this-page
   navs in terms.html:38 and transparency.html:37. A broadsheet panel is a rule
   and a wash, never a box with a radius (law 7). */
/* MEASURED, and it is exactly the false-friend failure the register predicted.
   catalog.css's .card is a subgrid ROW MEMBER: it carries
   `grid-template-rows: subgrid`. Resetting only `display` is not enough,
   because any later rule that turns grid back on inherits those tracks. On
   /transactions the form carries both `card` and `filter-bar`, `.filter-bar`
   sets display:grid, and the box came back as
   `grid-template-rows: subgrid [] [] [] [] []`: five inherited tracks stretched
   a 178px panel to 307px and put 129px of dead space under the Apply button.
   The tracks have to be cleared as well as the display. */
.card {
  display: block;
  padding: var(--sp-4) var(--sp-5);
  border: 0;
  border-block-start: var(--rule-mid);
  background: var(--c-sunken);
  grid-column: auto;
  grid-row: auto;
  grid-template-rows: none;
  grid-template-columns: none;
}
/* The selector is doubled up on purpose. catalog.css:1484 carries
   `.card h2, .card h3 { font-size: var(--t-sub) }`, which is (0,1,1) and beats
   a bare `.card__title` at (0,1,0). Measured on /admin/overrides/ui: the
   heading took this rule's uppercase and UI face but kept catalog's 30px, so
   "ADD OR UPDATE AN OVERRIDE" shouted across the panel in the wrong voice.
   The false friends have descendant rules, not only their own. */
.card__title,
.card h2.card__title,
.card h3.card__title {
  font-family: var(--face-ui); font-size: var(--t-micro);
  line-height: var(--lh-ui); letter-spacing: var(--tr-caps);
  text-transform: uppercase; color: var(--c-ink-muted);
}
/* An h3 card under an h2 band sits BELOW the band's head, one rung down.
   catalog.css's `.card h2, .card h3` sets both to --t-sub, which is right on
   the catalog home where the cards are the page's top level, and wrong on any
   platform page with band h2s: measured on /, the six band h2 rendered 28.5px
   (UA-sized, now fixed at the root) under card h3 at 30px, so the subordinate
   outranked its governor. Same specificity (0,1,1), this file loads later, so
   order decides. WRK-20260819T1401Z. */
.card h3 { font-size: var(--t-lead); }
/* --t-base, not --t-small: bible 4.1 gives 17px to "inputs, notes, card
   bodies" by name. The --t-small this line used to declare never rendered,
   because `.card p` at (0,1,1) supplied 17px over it; the declaration was
   wrong and the paint was right, which is how it survived. */
.card__body { font-size: var(--t-base); line-height: var(--lh-prose); }

/* The eyebrow is the broadsheet's kicker under ebomi's name. */
.eyebrow {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
}

/* An accent rule opened a section in the 2025 system. In the broadsheet a
   section is opened by a rule of graded weight and nothing else, so this is the
   heavy rule and carries no colour. */
.accent-rule { border: 0; border-block-start: var(--rule-heavy); margin: 0; }

/* The credibility block on /inside: the author's standfirst. It is a deck, not
   a callout, because it is credentials rather than a caution. */
/* Set one step above the body rather than two. At --t-sub against the body's
   --t-base the standfirst wrapped at roughly half the width of the prose that
   followed it, so the eye read two different measures on one page and the block
   looked like a pull quote from somewhere else. --t-lead keeps it clearly a
   standfirst and lets it share the body's column. */
.credibility {
  font-size: var(--t-lead); line-height: var(--lh-lead);
  color: var(--c-ink-strong); max-inline-size: var(--measure);
  padding-inline-start: var(--sp-5); border-inline-start: var(--rule-heavy);
}
.credibility__cite { font-size: var(--t-mini); font-style: italic; color: var(--c-ink-muted); }

/* The DOI chip is an identifier, so it takes the identifier face and the
   monospaced figure treatment the rest of the system gives accessions. */
.doi-chip {
  display: inline-block;
  position: relative;
  font-family: var(--face-ident); font-size: var(--t-mini);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
  /* --rule-control, not the hairline: this chip is a LINK, and law 7 says the
     hairline "may never be the sole boundary of a control". Measured by
     tools/ux/contrast.py on its first run: 1.5:1 light and 1.21:1 dark, an
     SC 1.4.11 miss on an operable boundary that the old stylesheet-level
     audit never enumerated because it never rendered a page. */
  padding: var(--sp-1) var(--sp-2); border: var(--rule-control);
  color: var(--c-ink);
}
/* 27px drawn, so it needed the floor. This one takes the invisible extension
   rather than real height, the opposite of the .nav-link decision, and the
   difference is whether neighbours can collide: a DOI chip is the only control
   in its block and nothing wraps beside it, so a halo contests no other point.
   Growing the box instead would put a 44px ruled rectangle around a citation
   line and make an identifier look like a button. */
.doi-chip::before {
  content: ""; position: absolute;
  inset-block: min(0px, calc((100% - 2.75rem) / 2));
  inset-inline: min(0px, calc((100% - 2.75rem) / 2));
}

/* .callout maps onto the broadsheet's .note: a heavy rule on the inline start
   and a wash, never a tinted box. The tone variants differ by rule STYLE, never
   by pigment (law 1), which is the same grammar the code chips use: solid for a
   statement, dashed for machinery, dotted for the routine and pre-committed. */
.callout {
  display: grid; gap: var(--sp-2);
  padding: var(--sp-3) 0 var(--sp-3) var(--sp-5);
  border-inline-start: var(--rule-heavy);
  background: var(--c-note-wash);
  max-inline-size: var(--measure);
}
.callout__title {
  font-family: var(--face-ui); font-size: var(--t-micro); font-weight: var(--w-bold);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
}
.callout--method  { border-inline-start-style: solid; }
.callout--warning { border-inline-start-style: dashed; background: transparent; }
.callout--success { border-inline-start-style: solid; }
.callout--danger  { border-inline-start-style: double; border-inline-start-width: 4px; background: transparent; }

/* .contents, the document index. Bible 13.7.

   The one component a platform document needs that the catalog never did: its
   own pages are short entity pages, and /transparency and /terms are long
   enough that a reader arrives looking for one section. It was drawn as
   `nav.card` with the links run together on one line, which is the 2025
   vocabulary and reads as a grey box of blue words (BUG-20260808T2305Z).

   Drawn instead as what a broadsheet uses for an index: a hairline-ruled list,
   one row per entry, the numeral in a fixed grotesque column and the title in
   the text face. The numeral is written in the markup rather than counted in
   CSS, because on /terms it is the clause number a reader quotes, and law 10
   prints a decision rather than inferring it.

   The row carries the 2.75rem target floor from bible section 8. Focus comes
   from the global :where(a…):focus-visible at catalog.css:471, so nothing is
   redefined here and bare :focus is never styled. */
.contents {
  display: grid;
  gap: var(--sp-3);
  padding-block: var(--sp-4);
  border-block: var(--rule-hair);
  max-inline-size: var(--measure);
}
.contents__label {
  font-family: var(--face-ui); font-size: var(--t-micro); font-weight: var(--w-bold);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
}
.contents ul { display: grid; list-style: none; margin: 0; padding: 0; }
.contents li { border-block-end: var(--rule-hair); }
.contents li:last-child { border-block-end: 0; }
.contents a {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: var(--sp-3);
  align-items: baseline;
  min-block-size: 2.75rem;
  align-content: center;
  padding-block: var(--sp-2);
  text-decoration: none;
  color: var(--c-ink);
}
.contents__no {
  font-family: var(--face-ui); font-size: var(--t-micro); font-weight: var(--w-bold);
  letter-spacing: var(--tr-caps);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--c-ink-muted);
}
/* The underline is the hover channel, not a colour change, so it survives
   greyscale and the achromatic chrome of law 1. */
.contents a:hover .contents__title { text-decoration: underline; text-underline-offset: 0.14em; }

/* Figures. Every quantity is set, not typed (law 3). .num and .unit are the
   platform's older names for what the broadsheet calls .fig. */
.num, .unit {
  font-family: var(--face-fig);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
}
.unit { color: var(--c-ink-muted); }
/* Doubled: see the .hint.hint comment. */
.t-md.t-md { font-size: var(--t-body); }

/* An unscoped .ident, which catalog.css only defines inside two containers. */
.ident {
  font-family: var(--face-ident); font-size: var(--t-mini);
  letter-spacing: var(--tr-flat); color: var(--c-ink-muted);
}

/* The Form 4 code chip. Direction is the one variable allowed a pigment
   (law 1), and it never carries alone (law 2): the code LETTER is inside the
   chip, and the rule style separates a decision to trade from compensation
   machinery, solid for the open market and dashed for everything the
   broadsheet's own grammar calls admin. In greyscale, and under every
   dichromacy, the letter and the rule style still say it. --c-buy, --c-sell
   and --c-admin are catalog.css's measured roles, carried not recomputed. */
.code-badge {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  min-block-size: 1.5rem; padding-inline: var(--sp-2);
  border: 1px solid var(--c-admin);
  border-radius: var(--radius);
  color: var(--c-admin);
  font-family: var(--face-ui); font-size: var(--t-micro);
  font-weight: var(--w-semi); letter-spacing: var(--tr-caps-snug);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
  white-space: nowrap;
  border-style: dashed;
  /* An inline-flex box's baseline is synthesized from its first flex item, and
     .code-badge__glyph is EMPTY in every template with no CSS content, so the
     chip was aligned on a box with no text in it. Measured in prose at 19px:
     the chip sat 13px above AND 13px below the baseline, centred on the
     baseline itself, while the surrounding text only descends about 4px. So it
     hung a full descender lower than anything beside it and inflated the line
     box from 30.8px to 34px, which is the "off" the owner saw on
     /transparency.

     `middle` aligns the box's centre with the baseline plus half the x-height,
     which is the text's own optical centre rather than its baseline. Measured
     after: 8px below, and the line box back to 30.8px, so the chip no longer
     changes the leading of the paragraph it sits in. A keyword rather than a
     nudge, so law 6 holds. No effect where the badge is a flex or grid item,
     which is every table and tape use (BUG-20260810T0012Z). */
  vertical-align: middle;
}
.code-badge--buy  { color: var(--c-buy);  border-color: var(--c-buy);  border-style: solid; }
.code-badge--sell { color: var(--c-sell); border-color: var(--c-sell); border-style: solid; }
/* A code the product has not yet classified prints dotted, the system's
   existing meaning for the pre-committed and routine, rather than guessing. */
.code-badge--planned { border-style: dotted; }
.code-badge__glyph { font-family: var(--face-fig); }


/* -----------------------------------------------------------------------------
   9. THE PLATFORM COMPONENTS
   -----------------------------------------------------------------------------
   Everything the account and admin surfaces need that a read-only anonymous
   catalog never did. Same policy as section 8: the names are the ones already
   in the templates, re-implemented in the broadsheet's language.
   -------------------------------------------------------------------------- */

/* --- Buttons. The old names are aliased onto the broadsheet's variants rather
       than redrawn, so there is exactly one button in the system and four ways
       to say its name. One .btn--ink per view (bible 8.3). --------------- */
.btn--primary   { --btn-face: var(--c-mark); --btn-label: var(--c-on-mark); --btn-rule: var(--c-mark); }
.btn--primary:hover  { --btn-face: var(--c-mark-hover); --btn-rule: var(--c-mark-hover); }
.btn--primary:active { translate: 0 1px; }
.btn--secondary:hover  { background: var(--c-hover-wash); --btn-rule: var(--c-mark); }
.btn--secondary:active { translate: 0 1px; }
.btn--ghost {
  --btn-rule: transparent; padding-inline: var(--sp-2);
  text-decoration: underline; text-decoration-thickness: 1px;
  text-underline-offset: 0.35em; text-decoration-color: var(--c-rule);
}
.btn--ghost:hover { text-decoration-thickness: 2px; text-decoration-color: currentColor; background: var(--c-hover-wash); }
/* --t-mini, not --t-micro: bible 8.3 reserves the micro step for table and
   card FEET, and the platform wears btn--sm on real controls, including the
   dashboard's whole action set. One rung up keeps them compact without
   labelling working buttons at folio size, which is the PI's "tiny font"
   complaint in its most literal form. WRK-20260819T1401Z decision 1's tail. */
.btn--sm { padding: var(--sp-2) var(--sp-3); font-size: var(--t-mini); min-block-size: 0; }
/* The small button's invisible hit extension, carried from catalog.css:913: it
   grows the pointer target to the 44px floor on any axis where the drawn box is
   smaller, and insets to nothing where it is not. */
.btn--sm::before {
  content: ""; position: absolute;
  inset-block: min(0px, calc((100% - 2.75rem) / 2));
  inset-inline: min(0px, calc((100% - 2.75rem) / 2));
}
.btn-group--nowrap { flex-wrap: nowrap; }

/* --- Tables. .table maps onto the broadsheet's ledger: no zebra, no cell
       borders, no container box. Hairlines between rows, a 2px rule under the
       head, a 3px rule opening the wrap (bible 8.5). ---------------------- */
.table { inline-size: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums lining-nums slashed-zero; }
.table thead th {
  position: sticky; inset-block-start: 0; z-index: 1;
  background: var(--c-canvas);
  font-family: var(--face-ui); font-size: var(--t-micro); font-weight: var(--w-semi);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted); text-align: start;
  padding: var(--sp-2) var(--sp-3);
  border-block-end: var(--rule-mid);
}
.table tbody td { padding: var(--sp-3); border-block-end: var(--rule-hair); vertical-align: baseline; }
.table tbody tr:last-child td { border-block-end: var(--rule-mid); }
.table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
.table tbody tr:hover, .table tbody tr:focus-within { background: var(--c-hover-wash); }
.table--compact tbody td, .table--compact thead th { padding: var(--sp-1) var(--sp-2); font-size: var(--t-mini); }
/* All three properties are load bearing: grid so the wrap cannot be sized by
   its content, relative so a scroll affordance can be positioned against it,
   and min-inline-size 0 so a grid child may actually shrink. */
.table-scroll { display: grid; position: relative; overflow-x: auto; min-inline-size: 0; border-block-start: var(--rule-heavy); }

/* THE STACKING TABLE, and this is a decision rather than a default.
   The alternative was to set the admin tables in agate at every width, which is
   the broadsheet's own answer to a dense table. It is rejected here for one
   measured reason: admin/academic.html DELIBERATELY mismatches
   data-label="Domain" against its "Claimed domain" header to avoid a measured
   390px ::before collision, and agate would make that carefully-placed
   workaround meaningless while reintroducing the collision it was written to
   dodge. Keeping the stack keeps a measured fix true. */
@media (max-width: 48rem) {
  .table--stack thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
  .table--stack tbody tr { display: grid; gap: var(--sp-1); padding-block: var(--sp-3); border-block-end: var(--rule-thin); }
  .table--stack tbody td { display: grid; grid-template-columns: 9rem 1fr; gap: var(--sp-3); padding: var(--sp-1) 0; border: 0; }
  .table--stack tbody td::before {
    content: attr(data-label);
    font-family: var(--face-ui); font-size: var(--t-micro);
    letter-spacing: var(--tr-caps); text-transform: uppercase;
    color: var(--c-ink-muted);
  }
  /* Every real child belongs in the VALUE column. Without this the td is a
     two-column grid whose first item is the ::before label, so a cell with two
     children put the first in column 2 and auto-placed the second into column 1
     of the next row, underneath the label rather than beside it.

     Measured at 390 on the dashboard's Company cell, which carries a ticker
     chip and a company name: the chip landed at left 164 and the name at left
     8, so the name sat outside the column its own label was naming. Legible,
     and wrong in the one way a labelled grid must not be
     (WRK-20260808T1132Z, "still open in this lane").

     They stack rather than sitting inline, which is the right answer at 390:
     a chip plus a full company name on one line is what forced the wrap in the
     first place. This puts both under the label, left-aligned, in the column
     that is theirs. */
  .table--stack tbody td > * { grid-column: 2; }
}

/* A STACK WHOSE VALUES ARE SENTENCES, not tabular data. `.table--stack` above
   sets a 9rem label column beside the value, which is right for the dashboard
   tables it was written for, where a value is a ticker, a date or a figure. On
   the privacy policy the values are prose: at 390 the label column leaves about
   200px for the text, roughly 25 characters a line, which is far under the
   measure the rest of the document is set to and turns a paragraph into a
   ladder.

   So the label goes ABOVE its value here rather than beside it, and the prose
   gets the full column. It is the same information in the same order, and the
   label keeps the kicker treatment so it still reads as a label rather than as
   a heading in the flow. Applied with `.table--stack--prose` rather than by
   changing `.table--stack`, because the dashboard's tables measured better the
   other way and this is an addition, not a correction (WRK-20260811T0042Z). */
@media (max-width: 48rem) {
  /* NOT A GRID, and that is the whole rule rather than a detail. `.table--stack`
     makes the cell `display: grid` so the ::before label can sit in its own
     track, and a grid BLOCKIFIES every child: an inline <a> or <strong> stops
     being part of the sentence and becomes its own grid item on its own line.
     Measured at 390 on the Turnstile row, whose "Turnstile Privacy Addendum"
     link computed to `display: block` and pushed the full stop after it onto a
     line of its own. Harmless-looking, and it is a sentence coming apart.

     So the prose cell is a plain block and the label is a block ::before above
     it. Same order, same treatment, and the text flows as text. */
  .table--stack--prose tbody td { display: block; padding: var(--sp-1) 0; }
  .table--stack--prose tbody td::before { display: block; margin-block-end: var(--sp-1); }
}

/* --- Empty, no-results and faulted are three states and must not look alike
       (law 8). The mark is what tells them apart, and it is generated so a
       template cannot ship the wrong glyph for its state. ----------------- */
/* No max-inline-size on the panel. An empty state that follows a table has to
   span the table it is speaking for; capped at --measure it drew a dashed box
   across the left half of a full-width header rule and read as an unfinished
   column rather than as a statement about the table. The BODY is capped
   instead, at 42ch, which is where the reading-measure argument actually
   applies. */
.empty-state {
  display: grid; gap: var(--sp-3); justify-items: start;
  padding: var(--sp-7) var(--sp-5);
  border: 1px dashed var(--c-rule-strong);
}
.empty-state__mark { font-size: var(--t-head); line-height: var(--lh-solid); color: var(--c-rule-strong); }
.empty-state__title { font-size: var(--t-lead); font-weight: var(--w-semi); color: var(--c-ink-strong); }
.empty-state__body { font-size: var(--t-small); line-height: var(--lh-prose); max-inline-size: 42ch; }
.empty-state__criteria { font-size: var(--t-mini); color: var(--c-ink-muted); font-family: var(--face-ident); }
/* A fault outranks a blank: a container empty because something broke never
   gets to claim that the world is empty. Doubled rule, not a pigment. */
.empty-state--error { border-style: solid; border-width: 2px; border-inline-start-width: 6px; }

/* --- Forms --------------------------------------------------------------- */
.field { display: grid; gap: var(--sp-2); }
.label {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
}
/* The required mark is a glyph AND the word, never the glyph alone, because an
   asterisk is a convention a first-time visitor has to already know. */
.label--required::after { content: " (required)"; color: var(--c-ink-faint); text-transform: none; letter-spacing: normal; }
.input, .select {
  font: inherit; color: var(--c-ink);
  background: transparent;
  border: 0; border-block-end: var(--rule-control);
  border-radius: var(--radius);
  padding: var(--sp-2) 0; min-block-size: 2.75rem;
  inline-size: 100%;
}
.input::placeholder { font-style: italic; color: var(--c-ink-disabled); }
.input:focus-visible, .select:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
/* Keyed to :user-invalid so an untouched field is never shown as an error. */
.input:user-invalid { border-block-end-style: double; border-block-end-width: 3px; }
.error-msg, .error-msg--inline {
  font-size: var(--t-mini); color: var(--c-ink-strong);
  display: flex; align-items: baseline; gap: var(--sp-2);
}
.error-msg::before, .error-msg--inline::before { content: "\2716"; font-family: var(--face-fig); }
.form-actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; padding-block-start: var(--sp-3); }
.filter-bar { display: grid; gap: var(--sp-3); padding-block: var(--sp-4); border-block-end: var(--rule-thin); }
.filter-bar__fields { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: end; }

/* --- Pagination ---------------------------------------------------------- */
.pagination { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; padding-block: var(--sp-4); border-block: var(--rule-hair); }
.pagination__status { margin-inline-start: auto; font-size: var(--t-mini); color: var(--c-ink-muted); font-variant-numeric: tabular-nums lining-nums slashed-zero; }
.result-count { font-size: var(--t-mini); color: var(--c-ink-muted); font-variant-numeric: tabular-nums lining-nums slashed-zero; }

/* --- The admin console --------------------------------------------------- */
/* The list is the flex container, not the <nav>. Flexing the nav alone left the
   six console links stacked vertically down the page, because the single <ul>
   child was the only flex item. */
.console-nav { padding-block: var(--sp-3); border-block-end: var(--rule-thin); }
.console-nav ul {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  list-style: none; margin: 0; padding: 0;
}
.stat-row { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); }
.stat-row--fill { grid-auto-flow: column; grid-auto-columns: 1fr; }
.stat-tile { display: grid; gap: var(--sp-1); padding: var(--sp-4); border-block-start: var(--rule-mid); background: var(--c-sunken); }
.stat-tile__label { font-family: var(--face-ui); font-size: var(--t-micro); letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--c-ink-muted); }
.stat-value { font-family: var(--face-fig); font-size: var(--t-head); line-height: var(--lh-solid); font-variant-numeric: tabular-nums lining-nums slashed-zero; color: var(--c-ink-strong); }
.stat-tile__meta { font-size: var(--t-mini); color: var(--c-ink-muted); }
/* stat-tile__state and tool__kind are asserted literally in
   tests/test_admin_console.py:193,220, so neither name may move. The state
   variants differ by rule style rather than pigment (law 1). */
.stat-tile__state { font-family: var(--face-ui); font-size: var(--t-micro); letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--c-ink-muted); }
.stat-tile--ok    { border-block-start-style: solid; }
.stat-tile--warn  { border-block-start-style: dashed; }
.stat-tile--error { border-block-start-style: double; border-block-start-width: 4px; }
.stat-tile--idle  { border-block-start-style: dotted; }

.tool-list { display: grid; gap: var(--sp-4); }
.tool { display: grid; gap: var(--sp-1); padding-block: var(--sp-3); border-block-start: var(--rule-hair); }
.tool__head { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: baseline; }
.tool__label { font-weight: var(--w-semi); color: var(--c-ink-strong); }
.tool__kind { font-family: var(--face-ui); font-size: var(--t-micro); letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--c-ink-muted); border: var(--rule-hair); padding-inline: var(--sp-2); }
.tool__path { font-family: var(--face-ident); font-size: var(--t-mini); color: var(--c-ink-muted); }
.tool__desc { font-size: var(--t-small); color: var(--c-ink); max-inline-size: var(--measure); }

/* --- Small parts --------------------------------------------------------- */
.disclosure { border-block-start: var(--rule-hair); }
.disclosure > summary { min-block-size: 2.75rem; display: flex; align-items: center; cursor: pointer; font-family: var(--face-ui); font-size: var(--t-small); }
.disclosure__body { padding-block-end: var(--sp-4); font-size: var(--t-small); line-height: var(--lh-prose); max-inline-size: var(--measure); }
.def-list { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-4); font-size: var(--t-small); }
.def-list dt { font-family: var(--face-ui); font-size: var(--t-micro); letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--c-ink-muted); }
.def-list dd { margin: 0; }
.ticker-chip, .role-badge {
  display: inline-flex; align-items: center;
  font-family: var(--face-ident); font-size: var(--t-micro);
  letter-spacing: var(--tr-flat);
  padding: var(--sp-1) var(--sp-2); border: var(--rule-hair);
  color: var(--c-ink-muted);
}
/* THE SIGN-IN PAGE IS ONE COLUMN AND EVERY BLOCK IN IT SHARES ITS EDGES.
   One task, one column: the heading, the form, the hint and the note all take
   the same measure and the column is centred. Anything that sets its own width
   inside here is overridden rather than argued with, because the whole point is
   that nothing here has an opinion of its own. */
.login {
  max-inline-size: 32rem; margin-inline: auto;
  /* ONE RHYTHM, DECLARED ONCE. Setting the width was half the fix and the PI
     found the other half: "That spacing on the sign up page, I'm pretty sure
     that's not following our design bible."

     He is right. The column was a plain block, so the gaps between its three
     parts were whatever each child's own margins happened to be: a large space
     under the heading, a small one under the form, and none at all between the
     hint and the note, which is why the note read as glued to the sentence
     above it. Three different gaps in a column of three things is not a
     rhythm, it is an accident.

     A grid with one gap token makes the spacing a property of the COLUMN
     rather than of its children, which is the argument the whole spacing scale
     rests on. */
  display: grid; gap: var(--sp-6); align-content: start;
}
/* The form and the sentence that qualifies it are ONE thing, so they sit closer
   than the column's rhythm. That difference is what tells a reader they belong
   together, and it is the only place here allowed a second value. */
.login .stack--sm { gap: var(--sp-3); }
.login > *, .login .stack > *, .login .hint, .login .note, .login .section-head p {
  max-inline-size: none;
}
.login .inline-capture__row { grid-template-columns: 1fr auto; }

/* THE REMOVE CONTROL. A word, quiet at rest and ruled when aimed at. The
   reasoning against the markup in templates/dashboard.html; what belongs here
   is that it is CONSTANT WIDTH, which is what makes the last column a column,
   and that it takes no pigment, because law 1 keeps the chrome achromatic even
   where the action is destructive. Ink and a rule carry "you are about to do
   something" perfectly well, and a red button on a row beside blue and rust
   trade figures is the collision law 1 exists to prevent. */
.btn-remove {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  min-inline-size: 5.25rem; min-block-size: 2rem;
  padding-inline: var(--sp-3);
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  background: none; border: var(--rule-hair); border-color: transparent;
  color: var(--c-ink-faint); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
/* The 44px floor as an invisible extension, so ten rows do not become ten
   buttons. */
.btn-remove::before {
  content: ""; position: absolute;
  inset: min(0px, calc((100% - 2.75rem) / 2));
}
/* --c-rule-strong, not --c-rule: the hover rule is the boundary of a control
   being aimed at, and law 7 says only the strong rule may bound something
   operable. Measured at --c-rule it was 2.04:1 light and 1.85:1 dark, below
   even the decorative bar; strong is 4.85 / 5.00 on this canvas. The rest
   state stays quiet by the PI's settled decision of 2026-08-19; the hover
   token was the one part left to the lane. WRK-20260819T1401Z. */
.btn-remove:hover { color: var(--c-ink-strong); border-color: var(--c-rule-strong); }
.btn-remove:focus-visible {
  outline: 2px solid var(--c-focus); outline-offset: 2px;
  color: var(--c-ink-strong);
}

/* A FIXED LANE, SO THE COMPANY NAMES START ON ONE LEFT EDGE.
   CVNA, FFAIW, FUSB, GMRE-PA and GTX are three, five, four, seven and three
   characters, so each chip was its own width and the column of names staggered
   down the page by up to 40px. The PI: "looks weird not aligned ... first and
   last columns shift. should be in a nice table".

   This is the identical defect the catalog's suggestion list was rebuilt to
   fix, and the identical fix: reserve the lane, centre the content in it. A
   ticker longer than the lane still grows it rather than being clipped, because
   an unreadable identifier is worse than a wobble. */
.watchlist__co .ticker-chip {
  flex: none; min-inline-size: 5rem; justify-content: center;
}
.mono { font-family: var(--face-ident); font-variant-numeric: tabular-nums lining-nums slashed-zero; }
.measure { max-inline-size: var(--measure); }
.lead { font-size: var(--t-lead); line-height: var(--lh-lead); color: var(--c-ink-strong); max-inline-size: var(--measure); }
/* Real block size to the 44px floor: a standalone link is a CONTROL wearing a
   paragraph's clothes, which is exactly what the old targets.py exemption
   could not see (it read the <p> wrapper as a sentence and suppressed every
   one of these). Real height rather than a hit extension because these sit in
   stacked prose flow where a halo would contest the paragraph above.
   WRK-20260819T1401Z. */
.link-standalone {
  display: inline-flex; align-items: center; min-block-size: 2.75rem;
  font-family: var(--face-ui); font-size: var(--t-small);
}
/* The preview card's action row: a wrapping LIST of controls, so bible 13.8
   sends it to real block size, same reasoning as the provenance strip. */
.row--loose a { min-block-size: 2.75rem; display: inline-flex; align-items: center; }
.card--sunken { background: var(--c-sunken); }
.row--between { justify-content: space-between; }
.row--baseline { align-items: baseline; }
.graded { display: grid; gap: var(--sp-2); }

/* Signed quantities. The pigment is the ONE variable it is allowed to carry,
   direction, and it never carries alone: the template also prints the sign and
   the accounting parentheses, so greyscale and every dichromacy still read it
   (law 2). */
.num--pos { color: var(--c-buy); }
.num--neg { color: var(--c-sell); }

/* --- Spacing utilities. The 2025 system's margin scale, mapped onto the
       broadsheet's space tokens so 206 call sites do not have to be edited.
       They are a compatibility surface, not a pattern to extend. ---------- */
.mt-3 { margin-block-start: var(--sp-3); }
.mt-5 { margin-block-start: var(--sp-5); }
.mt-6 { margin-block-start: var(--sp-6); }
.mt-7 { margin-block-start: var(--sp-7); }
/* Doubled: see the .hint.hint comment. */
.t-sm.t-sm   { font-size: var(--t-small); }
.t-2xs.t-2xs { font-size: var(--t-micro); }

/* catalog.css:2078 sets `.prose { display: grid }`, which is right when .prose
   wraps a run of blocks and WRONG when a template puts it on a single <p>:
   grid turns that paragraph's own inline content into grid items, so every
   inline link is torn onto its own row. transactions.html:53 does exactly
   that, and the sentence "…newest first. How it works explains the pipeline…"
   came out as three stacked lines. A paragraph is a paragraph. */
p.prose { display: block; }
p.prose > a { display: inline; }

/* catalog.css:2103-2109 styles `.prose p` and `.prose ul` and NEVER MENTIONS
   `ol`, so an ordered list inside prose takes the full container instead of the
   measure. Measured on /transparency at 1440px before this rule: the paragraphs
   set at 576px and 54-64 characters per line, the <ul> items at 576px and 47-63,
   and the seven <ol> items of "How a filing becomes an alert" at 1088px and
   94-124 characters per line. That list is the substantive content of the page
   whose whole argument is that the method can be read, and it was running at
   more than double the measure bible 4.3 sets (BUG-20260808T2305Z).

   The marker is the broadsheet's own section numeral rather than a browser
   decimal: same face, size, tracking and colour as .section-head__no, so a
   numbered procedure reads as a numbered procedure in the system's voice. It is
   zero-padded because a two-digit column does not re-indent at step 10.

   This lands here rather than in catalog.css because DEC-20260808T1149Z keeps
   that file unedited, and both templates carrying an <ol> inside .prose,
   transparency.html and academic.html, are platform pages. If a catalog
   template ever grows one, the rule moves rather than being duplicated. */
.prose ol {
  display: grid;
  gap: var(--sp-2);
  max-inline-size: var(--measure);
  list-style: none;
  counter-reset: prose-step;
}
.prose ol > li {
  position: relative;
  padding-inline-start: var(--sp-6);
  counter-increment: prose-step;
}
.prose ol > li::before {
  content: counter(prose-step, decimal-leading-zero);
  position: absolute;
  inset-inline-start: 0;
  /* Matched to the first line box of the body copy so the numeral sits on the
     text's own baseline rather than floating above it. Both factors are tokens,
     per law 6. */
  line-height: calc(var(--t-body) * var(--lh-prose));
  font-family: var(--face-ui);
  font-size: var(--t-micro);
  font-weight: var(--w-bold);
  letter-spacing: var(--tr-caps);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--c-ink-muted);
}


/* -----------------------------------------------------------------------------
   10. THE LANDING PAGE
   -----------------------------------------------------------------------------
   The one surface with components nothing else uses. It is the conversion
   page and the page a grant assessor is most likely to open, so it gets its
   own section rather than being squeezed into the compatibility layer.

   Its whole argument is that every figure can be checked, so the components
   here are built to carry provenance: the tape row links to a filing, the
   trade card names its accession and its retrieval time, and no slot reserves
   space for an analytic the schema cannot produce.
   -------------------------------------------------------------------------- */

/* Two columns and exactly two things: the proposition on the left, live
   evidence on the right. align-items:start so any residual imbalance falls at
   the bottom, where it reads as breathing room rather than as a hole. Below
   60rem it is one column, proposition first, because on a phone the evidence
   is what you scroll to after being told why to care. */
/* THE SIXTH FALSE FRIEND, and it was predicted. catalog.css:1728 puts the grid
   ON `.hero`; ebomi puts it one level down on `.hero__grid`. Leaving catalog's
   `display: grid` in place made `.hero` a single-column grid whose only item
   was `.hero__grid`, and a `minmax(0, 1fr)` column inside an auto-sized parent
   track resolves against content rather than against the page. Measured at
   1440: `.hero` was a correct 1088px while `.hero__grid` collapsed to 589px
   and each column to 262px, so the conversion surface sat in the middle third
   of the page with empty flanks. Block display hands the sizing back to the
   page track. */
.hero { display: block; padding-block: var(--band); }
.hero__grid { display: grid; gap: var(--sp-7); align-items: start; }
@media (min-width: 60rem) {
  .hero__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-8); }
}
.hero__proposition { display: grid; gap: var(--sp-4); align-content: start; }
.hero__sub { font-size: var(--t-lead); line-height: var(--lh-lead); color: var(--c-ink); max-inline-size: var(--measure); }
.hero__evidence { display: grid; gap: var(--sp-5); align-content: start; min-inline-size: 0; }

/* The email capture. One row on any width that fits it, because a field and
   its button that wrap onto two lines stop reading as one control. */
.inline-capture { display: grid; gap: var(--sp-2); }
.inline-capture__row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: end; }
.inline-capture__row .input { flex: 1 1 14rem; min-inline-size: 0; }

/* The live tape. A running column of hairline rows under a heavy rule, the
   same construction as the catalog's own tape so the two agree. */
.live-tape { display: grid; gap: 0; border-block-start: var(--rule-heavy); }
.live-tape__head {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  justify-content: space-between; align-items: baseline;
  padding-block: var(--sp-2);
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
  border-block-end: var(--rule-thin);
}
/* The tape head's THIRD state, where the store answered with real trades that
   have stopped advancing. The head is muted micro type by default, which is
   right for a neutral provenance stamp and exactly wrong for a flag, so this
   raises the ink and takes the pilcrow that `.stale-notice__kicker` already
   uses for a state word. Colour is not the only channel: the mark and the words
   "Not current" both carry it, so it survives greyscale (law 2), and the words
   carry it to a screen reader, which a border never would.
   BUG-20260817T1606Z. */
.tape-stale { color: var(--c-ink-strong); }
.tape-stale::before { font-family: var(--face-fig); content: "\00b6\00a0"; }

/* THE TRACKS LIVE HERE, ON THE LIST, AND NOT ON THE ROW. Each row used to be
   its own flex container, so the badge and the figure were pushed right as one
   content-sized group and a wider figure dragged the badge left with it.
   Measured on production at 1440: five rows put the group at x=1089 and the one
   seven-figure row put it at x=1075, so the column of badges was not a column
   (BUG-20260817T1604Z). The PI found it by eye; no gate saw it, and the
   misalignment was present at 390 and 834 as well.

   The rule directly below used to promise that the figures are "ranged right so
   a column of them aligns". Right-aligning was never enough to keep that
   promise: `tabular-nums` aligns digits WITHIN strings of equal length, and
   columns cannot align across rows that are not in the same grid. That is the
   same finding the watchlist reached at line 1530, and this takes the same
   remedy: the tracks go on the container and each row adopts them with
   `subgrid` while KEEPING ITS OWN BOX, so the hairline dividing two trades
   still has something to paint on. `display: contents` on the row would align
   the columns too and would delete that hairline.

   THE FALLBACK IS THE OLD FLEX ROW, DELIBERATELY. Where `subgrid` is
   unsupported the two declarations below are invalid, the row keeps
   `display: flex` from its own rule, and the reader gets exactly today's
   layout, misalignment included. Degraded to the status quo rather than
   broken. */
.live-tape__list {
  display: grid;
  grid-template-columns:
    max-content            /* the ticker chip */
    minmax(0, 1fr)         /* who traded, the only track that may shrink */
    max-content            /* the code badge, the column that was not one */
    max-content            /* the figure */
    max-content;           /* the share control */
  column-gap: var(--sp-3);
}
.tape-row {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  align-items: baseline;
  padding-block: var(--sp-3);
  border-block-end: var(--rule-hair);
  font-size: var(--t-small);
  position: relative;
}
.tape-row {
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  row-gap: var(--sp-2);
}
@supports (grid-template-columns: subgrid) {
  /* justify-items: start, or every item stretches to fill its track and the
     badge stops being the size of its own glyph. The figure opts back out
     below, because it is the one thing that must sit at the track's end. */
  .tape-row { display: grid; align-items: baseline; justify-items: start; }
  /* No box of its own, so its two children take tracks 3 and 4 directly. */
  .tape-value { display: contents; }
  .tape-amount { justify-self: end; }
}
/* BELOW 48rem THE ROW IS TWO LINES, and it was two lines before this change:
   measured 88px at 390 against 55px at 834. Five tracks do not fit 390, so the
   line break is kept and stated as placement rather than left to `flex-wrap`,
   which cannot align anything across rows. Who traded spans to the end on line
   one; the badge and the figure hold tracks 3 and 4 on line two, which is what
   makes them align at 390 exactly as they do at 1440. */
@media (max-width: 47.999rem) {
  @supports (grid-template-columns: subgrid) {
    .tape-row > .ticker-chip { grid-column: 1; grid-row: 1; }
    .tape-row__who { grid-column: 2 / -1; grid-row: 1; }
    /* THE SECOND LINE STARTS WHERE THE NAME STARTS.
       It began in track 3, so under the ticker chip and the whole name track
       there was a void the width of two columns, and every row on the phone
       carried the same hole. That is what the PI's photograph shows and what
       "the entire page is a mess" is mostly made of: not one broken element,
       six rows each with a gap where a reader expects a line to begin.

       The badge now opens the line at track 2, level with the name above it,
       and the figure and the share control keep their own tracks so they still
       align down the column exactly as they do at 1440. Nothing moved on the
       desktop layout; only the phone's second line changed where it starts. */
    .tape-row .code-badge { grid-column: 2; grid-row: 2; }
    .tape-amount { grid-column: 4; grid-row: 2; }
    .tape-row .share { grid-column: 5; grid-row: 2; }
  }
}
/* The figure was ending exactly on the row's right edge, measured at +0.0px on
   production, so the last digit sat hard against the end of the hairline that
   bounds the row and read as running past it. The catalog's own ledger, which
   is the reference for this component, leaves 12px there. Matching it rather
   than inventing a number (BUG-20260810T0012Z). */
/* On the FIGURE, not on the group. Padding is not inherited, so a
   `display: contents` wrapper cannot carry it, and the figure is the last thing
   in the row under both the subgrid layout and the flex fallback, so the
   clearance lands in the same place either way. */
.tape-amount { padding-inline-end: var(--sp-3); }
/* The filing link measured 21px in a row that is already 54px, so the target
   was the only thing missing and the space for it was already there. The hit
   extension takes it to 44px and still leaves 5px of clearance to the row
   above and below, so no two targets touch. Real height would have been wrong
   here: it would push a dense tape row to 44px of text and turn a listing into
   a list of buttons, which is the density ladder's whole objection. */
.tape-row__who a { position: relative; }
.tape-row__who a::before {
  content: ""; position: absolute;
  inset-block: min(0px, calc((100% - 2.75rem) / 2));
  inset-inline: min(0px, calc((100% - 2.75rem) / 2));
}
.tape-row__who { flex: 1 1 12rem; min-inline-size: 0; }
.tape-row__meta { color: var(--c-ink-muted); font-size: var(--t-mini); }
/* The value is a quantity, so it is set rather than typed (law 3). It is also
   ranged right, and THAT ALONE NEVER MADE A COLUMN OF THEM ALIGN, which is what
   the older version of this comment claimed. Alignment comes from the shared
   tracks declared on `.live-tape__list` above; `text-align: end` only decides
   where the digits sit inside the track once the track exists.

   The font properties stay on the group even though the group has no box under
   `display: contents`, because inheritance still flows through such an element
   and the figure is inside it under both layouts. `margin-inline-start: auto`
   is the flex fallback's own right-push and is inert under the grid. */
.tape-value {
  margin-inline-start: auto;
  font-family: var(--face-fig);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
  white-space: nowrap;
}
.tape-amount { text-align: end; white-space: nowrap; }

/* The worked trade card: one filing explained all the way through. */
.trade-card {
  display: grid; gap: var(--sp-4);
  padding: var(--sp-5);
  border-block-start: var(--rule-heavy);
  background: var(--c-sunken);
}
/* THE HEAD IS A GRID WITH THE NOTE'S PANEL ON ITS OWN FULL-WIDTH ROW.
   Measured before this (2026-08-19, WRK-20260819T1401Z): opening the "?"
   grew the card 46.5 percent at 1440 and threw the code badge 375px left and
   99px down, because the disclosure's panel opened INSIDE the head's flex
   row, stretched its flex item to the panel's measure, and space-between
   re-laid the whole row. Two thirds of the growth was the control being
   forced onto its own full-width line, not the sentence.

   So the panel gets a full-width row of its own, by construction: the badge
   cluster and the details are `display: contents`, the head places identity
   in column 1 spanning the first two rows, badge and summary in columns 2
   and 3 of a 44px-tall first row (centred on EACH OTHER, which reinstates
   the measured fix for the badge sitting 13px above its note), and the panel
   spans every column on a later row. Opening now moves nothing except the
   text arriving: re-measured, badge displacement 0x0 at both 390 and 1440,
   growth is the sentence alone. */
.trade-card__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content max-content;
  column-gap: var(--sp-3);
  align-items: start;
}
.trade-card__head > div:first-child { grid-column: 1; grid-row: 1 / 3; }
.trade-card__head .row--tight,
.trade-card__head .method-note { display: contents; }
/* Chromium 131+ slots a details' content into a UA ::details-content box, so
   without this line the grid item is that anonymous box, auto-placed and
   content-sized, and the panel rendered 37px wide at one character per line
   (measured; the growth read 196 percent and it was all height). Engines
   without the pseudo-element parent the body directly and ignore this rule,
   which is the correct behaviour in both worlds. */
.trade-card__head .method-note::details-content { display: contents; }
.trade-card__head .code-badge {
  grid-column: 2; grid-row: 1; align-self: center;
}
.trade-card__head .method-note > summary {
  grid-column: 3; grid-row: 1; align-self: center;
}
.trade-card__head .method-note__body {
  grid-column: 1 / -1; grid-row: 3;
  margin-block-start: var(--sp-2);
  padding-block-start: var(--sp-3);
  border-block-start: var(--rule-hair);
  /* The pair below is one idiom: CONTRIBUTE NOTHING, THEN FILL THE ROW. The
     panel's preferred width is the prose measure, and on the hero the card
     sits in a content-sized track, so a naive panel INFLATES ITS OWN CARD on
     open: measured, the head grew 464 to 644 and the document scrolled 18px
     sideways. inline-size: 0 removes the panel from every intrinsic sizing
     pass up the chain; min-inline-size: 100% then hands it exactly its grid
     area once the tracks are settled. The sentence wraps to the card, never
     the card to the sentence. */
  inline-size: 0;
  min-inline-size: 100%;
}
/* The phone keeps the shape it had: identity full-width, then the badge and
   its note on their own left-anchored row, then the panel. Defined at both
   widths deliberately, never inherited (the watchlist lesson). */
@media (width < 30rem) {
  .trade-card__head { grid-template-columns: max-content max-content minmax(0, 1fr); }
  .trade-card__head > div:first-child { grid-column: 1 / -1; grid-row: 1; }
  .trade-card__head .code-badge { grid-column: 1; grid-row: 2; margin-block-start: var(--sp-2); }
  .trade-card__head .method-note > summary { grid-column: 2; grid-row: 2; margin-block-start: var(--sp-2); }
  .trade-card__head .method-note__body { grid-row: 4; }
}
.trade-card__insider { font-size: var(--t-lead); font-weight: var(--w-semi); color: var(--c-ink-strong); }
.trade-card__role { font-size: var(--t-mini); color: var(--c-ink-muted); }
/* SHARES AND PRICE SIDE BY SIDE, TOTAL ACROSS BOTH.
   The previous rule was `repeat(auto-fit, minmax(min(100%, 11rem), 1fr))`,
   which sounds adaptive and measured as a defect: the card is 512px on the
   hero and three 11rem tracks need 528 plus gaps, so it wrapped to TWO columns
   with Total value orphaned on a row of its own at every desktop width. The PI:
   "that box is also a super hodgepodge. I don't even know what that is, but
   it's really all over the place."

   He is right and the orphan was the whole of it. Two columns and a spanning
   third is not a compromise, it is the correct hierarchy: shares and price are
   the two inputs and belong beside each other for comparison, and the total is
   what they produce.

   IT ALSO SOLVES THE OVERFLOW THAT SHAPED THE OLD RULE. A total like
   $1,480,250.00 is a single unbreakable 219px token in the figure face, which
   is why a three-track layout scrolled sideways at 390px. The widest token now
   sits in the widest cell by construction rather than by luck.

   One column below 48rem, where the hero stacks and the card is 350px: two
   tracks there would be 155px each, under the token, and law 5 forbids the
   sideways scroll that follows. */
.trade-card__figures {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4); margin: 0;
}
@media (min-width: 48rem) {
  .trade-card__figures { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .trade-card__figures > :last-child { grid-column: 1 / -1; }
}
/* The total is the figure the card exists to report, so it is set larger and
   separated by a hairline rather than by a heading. Law 3: a quantity earns its
   size from what it means, not from where it sits.

   THAT SENTENCE WAS FALSE FOR NINE DAYS: this rule said --t-lead, a step
   BELOW the --t-sub the two inputs took, so the headline figure was the
   smallest of the three and the comment asserted the opposite. The 24px was
   doing undocumented protective work against corrupt magnitudes ($1.6e15 in
   the hero's own window), which is now done honestly: the server flags a
   total past LANDING_MAGNITUDE_CEILING, the card refuses to feature one, and
   the --suspect variant below takes the compact size. So the inputs step down
   to --t-lead, the product takes --t-sub, and the hierarchy the comment
   always claimed is finally the one that renders. WRK-20260819T1401Z. */
.trade-card__figures > :last-child {
  border-block-start: var(--rule-hair);
  padding-block-start: var(--sp-3);
}
/* align-content: start, or a figure that gains a third line pushes every OTHER
   figure's number down. The cells are stretched to a common height by the grid
   above, and rows that are merely `auto` then share the slack out, so adding
   the "weighted across N lines" note under the price dropped the share count
   off the price's baseline. Measured when the note shipped
   (BUG-20260817T1605Z). The figures are a row of quantities and they read as a
   row only while they sit on one line. */
.trade-card__figure {
  display: grid; gap: var(--sp-1); min-inline-size: 0;
  align-content: start;
}
.trade-card__figure dt {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase; color: var(--c-ink-muted);
}
.trade-card__figure dd {
  margin: 0;
  font-family: var(--face-fig); font-size: var(--t-lead);
  line-height: var(--lh-solid);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
  color: var(--c-ink-strong);
}
.trade-card__figure--total dd { font-size: var(--t-sub); }
/* As filed, sized so a 25-character filing error cannot drag the page
   sideways. Law 3 forbids wrapping a quantity, so the defence is size. */
.trade-card__figure--suspect dd { font-size: var(--t-base); }

/* Provenance. The accession and the retrieval time, in the identifier face,
   because they are strings to be matched against EDGAR rather than read. */
.provenance {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  align-items: baseline;
  font-size: var(--t-mini); color: var(--c-ink-muted);
  padding-block-start: var(--sp-3); border-block-start: var(--rule-hair);
}
/* p.prose is how the templates actually write the reading class: on the
   paragraph itself. catalog.css:2891 caps `.prose p`, the DESCENDANT, so the
   landing's and /transactions' lede paragraphs computed max-inline-size: none
   and ran 118-122 characters a line at 1440. Declared beside its cause's twin
   rather than fixing each template, because both shapes are legitimate and
   the sheet should serve both. NO margin-inline: auto: centring would pull a
   lede away from the heading it belongs to (recorded in the audit).
   WRK-20260819T1401Z. */
p.prose { max-inline-size: var(--measure); }
/* REAL BLOCK SIZE, NOT A HIT EXTENSION, for the provenance links: they sit in
   a WRAPPING metadata strip, and bible 13.8 sends a wrapping list to real
   height because a halo would contest the row above. Measured 22px tall
   before this; the extension route also costs more vertical space than the
   block (211.00 against 176.91px). These are the two targets the old
   targets.py exemption suppressed while printing a clean pass. */
.provenance a { min-block-size: 2.75rem; display: inline-flex; align-items: center; }
.provenance__label {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
}
.provenance__accession { font-family: var(--face-ident); letter-spacing: var(--tr-flat); }

/* The labelled sample notice. Dashed, because dashed means machinery rather
   than decision, and it is the state where the machinery did not answer. It
   must never be quiet: a page showing specimen rows has to say so louder than
   the rows say anything. */
.stale-notice {
  display: flex; gap: var(--sp-2); align-items: baseline;
  padding: var(--sp-3) var(--sp-4);
  border: 2px dashed var(--c-rule-strong);
  font-size: var(--t-mini); line-height: var(--lh-prose);
  color: var(--c-ink);
}
/* The state word, in the kicker treatment 13.2 uses on .notice. It is real
   text and not aria-hidden generated content, because the word is what
   carries the state to a screen reader; the rule carries it to everyone
   else. `nowrap` because "No filings yet" breaking across two lines inside a
   baseline-aligned flex row drags the body copy down with it. */
.stale-notice__kicker {
  flex: none; white-space: nowrap;
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
}
.stale-notice__kicker::before { font-family: var(--face-fig); content: "\00b6\00a0"; }
/* Empty is not faulted (BUG-20260808T1131Z). Same panel, solid rule: the
   store answered and had nothing to say, which is a stated fact rather than
   machinery failing. One declaration, and it survives greyscale because it is
   a difference of shape (law 2). */
.stale-notice--empty { border: var(--rule-mid); }

.method-note { display: grid; gap: var(--sp-2); padding-block: var(--sp-3); border-block-start: var(--rule-hair); }
/* The "?" disclosure measured 28x31. Real size rather than a hit extension:
   its neighbour is the code badge in a `.row--tight`, one --sp-2 (8px) away,
   and a halo on a 28px box reaches exactly 8px each side, so an extension
   would put two targets in contact. Section 8 allows either mechanism and
   forbids the contact. */
.method-note > summary {
  display: inline-flex; align-items: center; justify-content: center;
  min-inline-size: 2.75rem; min-block-size: 2.75rem;
  /* A CONTROL, NOT PUNCTUATION (WRK-20260819T1401Z). Measured before this:
     rest and hover were identical in all 25 probed properties including
     cursor: auto, and the glyph sat in the prose face at prose size, so the
     one interactive mark on the card read as a stray question mark. The ui
     face is the furniture voice (bible section 4), the cursor says operable,
     and the wash on hover is the same state every button already has. The
     :focus-visible ring was already correct and keyboard users had the
     control to themselves; everyone else now gets to know it exists. */
  font-family: var(--face-ui); font-size: var(--t-base);
  color: var(--c-ink-muted);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.method-note > summary:hover {
  background: var(--c-hover-wash); color: var(--c-ink-strong);
}
/* --t-base, not --t-small: this is 761 characters of running sentences, and
   4.1's --t-small row reads "UI labels, buttons, ledger data, tape rows".
   Notes are --t-base's own listed use. Same correction as .faq__answer. */
.method-note__body { font-size: var(--t-base); line-height: var(--lh-prose); max-inline-size: var(--measure); }

.faq { display: grid; }
.faq details { border-block-start: var(--rule-hair); }
.faq details:last-child { border-block-end: var(--rule-hair); }
.faq summary {
  min-block-size: 2.75rem; display: flex; align-items: center; gap: var(--sp-3);
  cursor: pointer; font-size: var(--t-small); font-weight: var(--w-medium);
  transition: background-color var(--dur-fast) var(--ease-out);
}
/* The same missing hover the "?" had; a disclosure that changes nothing under
   the pointer is invisible to everyone who does not already know it opens. */
.faq summary:hover { background: var(--c-hover-wash); }
/* --t-base: running prose, same correction and reason as .method-note__body. */
.faq__answer { padding-block: 0 var(--sp-4); font-size: var(--t-base); line-height: var(--lh-prose); max-inline-size: var(--measure); }

/* The closing band. It is `full`, so it spans the viewport, and its contents
   have to be brought back onto the page track or they sit against the left
   edge of the screen. */
.cta-band {
  background: var(--c-sunken);
  border-block-start: var(--rule-heavy);
  padding-block: var(--band);
  text-align: center;
}
.cta-band > * {
  inline-size: min(100% - (2 * var(--gutter)), var(--measure));
  margin-inline: auto;
}
.cta-band .btn-group { justify-content: center; }
/* Stacked and equal on a phone. Side by side at 390 the two labels made the
   ghost 41 percent wider than its lead (253.1 against 179.3px), presenting
   four vertical edges matching none of the page's three dominant left edges.
   Stacked full-width, both buttons share the band's own edges, which is the
   convention every closing band on Stripe and Render follows. */
@media (max-width: 30rem) {
  .cta-band .btn-group { display: grid; }
  .cta-band .btn-group .btn { inline-size: 100%; }
}
.btn--lg { padding: var(--sp-4) var(--sp-6); font-size: var(--t-small); }

.row--loose { gap: var(--sp-5); }
/* The Load more foot: one centred control under the ledger, its status line
   beneath it. The anchor is a .btn--ruled, the default action treatment;
   this only composes the two. */
.load-more { display: grid; justify-items: center; gap: var(--sp-2); padding-block: var(--sp-4); }
.w-semibold { font-weight: var(--w-semi); }


/* -----------------------------------------------------------------------------
   11. THE DASHBOARD
   -----------------------------------------------------------------------------
   The screen 925 of 928 registered accounts will see when they first arrive,
   and the one where choosing a company is the product's activation event
   (DEC-20260808T1024Z). Every class name here is the one already in
   dashboard.html: the template resolves 21 ids through unguarded
   getElementById and queries .listbox__option and .combobox by class, so a
   rename breaks behaviour with no test failure.
   -------------------------------------------------------------------------- */

/* THE ACTIVATION BLOCK, shown only while the reader has chosen nothing.

   928 accounts registered and three have ever tracked a company. The empty
   dashboard is therefore the product's most-visited authenticated screen and
   its only conversion surface, and until now it was a dashed panel under the
   table saying "No companies tracked yet" while the loudest control on the
   page said "Manage billing".

   It is a STATEMENT, not a card. No wash, no radius, no shadow: it is opened
   by the heavy rule the broadsheet uses to open a section (law 7), so it reads
   as the top of the page rather than as an interruption on it. The one place
   it raises its voice is the title, set at --t-sub, because on this screen the
   instruction is the most important line on the page and the h1 above it is
   just the room's name.

   It carries no button. The control it wants the reader to use is the search
   input in the card immediately below, and a button here would either
   duplicate that control or scroll to it, which is a page arguing with
   itself. Focus is moved into the input instead, by the template, and only
   when the list is empty. */
.start {
  display: grid; gap: var(--sp-3);
  padding-block: var(--sp-5) var(--sp-6);
  border-block-start: var(--rule-heavy);
}
.start__title { font-size: var(--t-sub); line-height: var(--lh-tight); color: var(--c-ink-strong); }
/* --measure, not 42ch. This is the page's argument for doing the thing, so it
   is set as prose at the system's reading measure, unlike the 42ch caps on
   .blank and .empty-state, which are labels on a container rather than copy. */
.start__body { font-size: var(--t-base); line-height: var(--lh-prose); max-inline-size: var(--measure); }
.start__body strong { font-weight: var(--w-semi); color: var(--c-ink-strong); }
/* The two escape hatches for a reader who cannot name a company from memory,
   which on this product is most of them. They point at /issuers and /tape,
   which are public, anonymous and already built.

   DELIBERATELY NOT display:flex, and this cost a measured defect. Drawn as a
   wrapping flex row it rendered "see what insiders filed today ." with a gap
   before the full stop, because the trailing text node is a flex item and
   takes the container's gap like any other. This is the same trap `.prose`
   sets one file over: a layout display value turns the inline content of a
   sentence into boxes, and a sentence is not a set of boxes. Anything that is
   prose with links in it stays a block. */
.start__browse {
  font-size: var(--t-small); color: var(--c-ink-muted);
  line-height: var(--lh-prose);
  max-inline-size: var(--measure);
  padding-block-start: var(--sp-2);
}
/* .link-standalone is display:inline-block, which is correct where it is a
   standalone call to action and wrong mid-sentence: an inline-block cannot
   break across lines, so at 390 a long link name pushed the line over rather
   than wrapping inside it. */
.start__browse .link-standalone { display: inline; }

/* THE 404, 403 AND 500 PAGES.

   These were the last user-facing surfaces on no design system at all: before
   templates/error.html existed a browser was handed `{"detail":"Not Found"}`
   as JSON.

   The status number is the page's own figure and is set as one, in the figure
   face with tabular figures, because on this site a number is a thing you can
   check. It is the eyebrow rather than the headline: the reader needs the
   sentence first and the code second, and a giant 404 is decoration that says
   less than four words of English.

   No pigment (law 1). A 500 is not red here and a 404 is not orange. The three
   states are told apart by their number and their sentence, which is the same
   answer 13.2 gives for notices and 8.10 gives for containers, and it is the
   only answer available in a system whose two pigments mean buy and sell. */
.errorpage { padding-block: var(--sp-6) var(--sp-8); }
.errorpage__status {
  font-family: var(--face-fig);
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
}
.errorpage__body { max-inline-size: var(--measure); }
/* A list of routes out, ruled like the rest of the system rather than bulleted.
   The rule is the hairline, because these are alternatives of equal weight and
   nothing here is a recommendation. */
.errorpage__ways ul { list-style: none; margin: 0; padding: 0; display: grid; }
.errorpage__ways li { border-block-start: var(--rule-hair); }
.errorpage__ways li:last-child { border-block-end: var(--rule-hair); }
.errorpage__ways a { display: block; padding-block: var(--sp-3); min-block-size: 2.75rem; }

/* THE TURNSTILE WIDGET, which is the one thing on this site the design system
   does not draw.

   Cloudflare renders it into a cross-origin iframe, so nothing in this file
   can reach inside it: not the face, not the rule weights, not the corner
   radius. That is the point of the control and it is not negotiable. What is
   left is the box it sits in, and the honest thing to do with a foreign
   object is give it room rather than pretend it belongs.

   `min-block-size` is reserved before the script answers. The iframe arrives
   asynchronously, and without a reservation the Continue button jumps down
   the page under the reader's cursor at the moment they are reaching for it.
   65px is the rendered height of the `managed` widget; the value is a
   reservation and not a cap, so a widget that grows into its interactive
   state pushes the layout rather than being clipped, which is law 5's rule
   for a reservation never lying about content.

   No border and no wash. It carries its own frame, and putting a second one
   around it would read as two nested boxes. */
/* COLLAPSED AT REST, EXPANDED ON CLOUDFLARE'S OWN SIGNAL. The widget runs
   data-appearance="interaction-only", so for almost every visitor nothing is
   ever painted here, and the old 65px reservation put 117.8 CSS px of zero
   ink between the CONTINUE button and the sentence under it, the largest
   ink-free run on the page (law 5: a reservation may never overstate what it
   holds). It was also 8.77px SHORT of the 73.77px a real challenge paints,
   so it managed to be too big and too small at once. No CSS can see the
   truth: the iframe sits in a CLOSED shadow root, so :has(iframe) can never
   match and the old `.turnstile iframe` rule below it never matched anything
   (deleted). data-before-interactive-callback stamps [data-challenge] before
   a visible challenge renders, and only then does the slot open, at the size
   a challenge actually paints. WRK-20260819T1401Z. */
.turnstile { min-block-size: 0; margin-block-start: 0; }
.turnstile:not([data-challenge]) .cf-turnstile { max-block-size: 0; overflow: clip; }
.turnstile[data-challenge] {
  min-block-size: 74px;
  margin-block-start: var(--sp-3);
}

/* FEEDBACK IS PRINTED, NOT FLOATED (law 11). The region was
   `position: fixed` in the bottom-right corner at z-index 90. It is now a
   block in the document, which fixed the half of the law everybody quotes: it
   cannot be scrolled away from, it sits over nothing it does not own, and it
   needs no z-index anybody has to reason about.

   THE DASHBOARD NO LONGER USES IT (DEC-20260810T0652Z), and the reason is the
   other half of the law. Bible 13.2 requires feedback "printed into the
   document AT THE POINT OF CONSEQUENCE", and this region was the LAST element
   on that page, so a failure caused by the combobox at the top printed below
   the entire watchlist. The dashboard now prints a `.notice` into the band
   whose control produced the message. The comment here used to cite
   dashboard.html:169 as proof the markup already did the right thing; it put
   the region in the document and at the bottom of it, which is why the
   citation was worth less than it looked.

   AND THE RULES ARE NOW DELETED, which is what bible 13.2 asked for in the
   first place: "it is deleted rather than restyled". The dashboard was the
   ONLY caller. A grep across templates, ebomi/ and every generated document
   for `toast-region`, `.toast` and `toast--` returns nothing outside this
   file and the records that discuss it. Keeping an unreferenced component
   because "other surfaces might use it" was an assumption rather than a
   measurement, and it was wrong; the stylesheet and the bible disagreed for
   eleven days only because nobody checked which one had the callers.

   `.notice` is the component. It carries the same four states, told apart by
   kicker word, rule style and mark rather than by pigment, and it is declared
   in section 3 of this file. */

/* THE DESTRUCTIVE CONTROL. catalog.css has no danger variant and that is law 1
   holding, not an oversight: the two pigments carry direction on transactions
   and nothing else may take either.

   MEASURED CORRECTION, and it corrects bible 13.3's own drawing. The first
   attempt struck the label through, as .btn--struck does. Rendered on the
   dashboard it read as CANCELLED rather than as destructive: a button whose
   word "REMOVE" is crossed out looks like a Remove that has been called off,
   which is the opposite of the meaning. The strike only works when the label
   IS the object being destroyed, "Remove Alfa Fixture Corp", because then the
   line falls across the company name. It cannot carry the meaning on a bare
   verb.

   So .btn--danger takes the doubled rule instead, which is the same shape
   difference .notice--refused and .empty-state--error use for "this one is not
   like the others", and it survives greyscale and every dichromacy.

   PAID, 2026-08-10 (DEC-20260810T0652Z), and RE-PAID 2026-08-19: renderRow's
   accessible name is "Remove ALFA", which names the object under 13.3 AND
   contains the visible word REMOVE per WCAG 2.5.3. (This comment previously
   claimed "Stop watching ALFA", which was true for nine days and shared no
   word with the visible label; tools/ux/labels.py now gates the class of
   defect, and the render, not this comment, is the truth. Bible 12.2.) */
.btn--danger {
  --btn-rule: var(--c-rule-strong);
  --btn-rule-w: 3px;
  border-style: double;
}
.btn--danger:hover { background: var(--c-hover-wash); }

/* The per-company alert switch. A switch is chrome rather than data, so it may
   take the mark colour; it is also never the only channel, because the control
   is labelled and exposes aria-checked. */
.switch { display: inline-flex; align-items: center; min-block-size: 2.75rem; cursor: pointer; }
/* The control IS a real checkbox: dashboard.html's renderRow builds
   <label class="switch"><input type="checkbox"><span class="switch__track">.
   The input is hidden from paint and kept in the accessibility tree and the
   tab order, so the state is driven from :checked rather than from an
   aria-checked attribute nothing sets. Styling this as if it were a button
   left the raw checkbox drawn next to the track, which is what shipped in the
   first capture. */
.switch input[type="checkbox"] {
  position: absolute; opacity: 0; inline-size: 0; block-size: 0; margin: 0;
}
.switch input:checked + .switch__track { background: var(--c-mark); }
.switch input:checked + .switch__track::after { translate: 1.15rem 0; background: var(--c-on-mark); }
.switch input:focus-visible + .switch__track { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.switch__track {
  inline-size: 2.75rem; block-size: 1.5rem;
  border: var(--rule-control); border-radius: var(--radius);
  position: relative; display: block;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.switch__track::after {
  content: ""; position: absolute;
  inset-block: 2px; inset-inline-start: 2px;
  inline-size: 1.1rem; background: var(--c-ink);
  transition: translate var(--dur-fast) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .switch__track, .switch__track::after { transition: none; }
}

/* THE WATCHLIST, which replaces the tracked-companies table
   (DEC-20260810T0652Z).

   It was a <table class="table table--stack">. Five treatments were rendered
   into a live /dashboard against these stylesheets at the real viewports, three
   companies each, with the labels that ship:

     variant                                390 height   1440 left edges  columns
     A  the table that shipped                  763px          9          aligned
     B  ruled list, controls in a flex row     373.7px        13          ragged
     C  B, grid columns on each <li>           373.7px        13          ragged
     D  columns on the <ul>, subgrid on <li>   373.7px         7          aligned
     E  D with the action ranged right         373.7px        11          ragged

   D is this. It is 51 percent shorter at 390 than the table and has FEWER
   distinct left edges at 1440 than the table it replaces.

   WHY A LIST AND NOT A TABLE. A watchlist row is one entity and the controls
   that act on it. The only cross-row comparison a reader makes is "which of
   these email me", which survives as long as the switches align.
   `.table--stack` exists to make genuinely tabular data readable at 390, and
   applied to a list of objects it produced FOUR visual lines per company: a
   9rem label column printing COMPANY, TRACK and an empty third label beside a
   chip, a name, a switch and a button.

   WHY SUBGRID, AND THIS IS THE PART THAT WAS MEASURED RATHER THAN ASSUMED.
   Variant C declared the three tracks on `.watchlist__item` and measured
   BYTE-IDENTICAL to B. That is a result and not a harness fault: each item is
   its own grid, so a `max-content` track is sized against one company's
   content, and columns cannot align across rows that are not in the same grid.
   Column alignment is the one thing a <table> gets for free and a list has to
   ask for. The tracks therefore live on `.watchlist` and each item adopts them
   with `subgrid` while KEEPING ITS OWN BOX, so the rule dividing two companies
   still has something to paint on. Making the item `display: contents` would
   align the columns too and would delete that box.

   THE FALLBACK IS THE MOBILE LAYOUT, DELIBERATELY. Where `subgrid` is not
   supported the declaration is invalid, the item keeps its single-column grid,
   and a wide viewport gets the stacked shape. That is degraded rather than
   broken, and it is the shape every reader below 48rem already gets. */
/* The account meta line: the address and the plan, adjacent. They used to sit
   at opposite ends of a `.row--between` 1088px wide at 1440, which put the one
   fact identifying the reader and the one fact naming their allowance as far
   apart as the page permits.

   The chip needs its own gap and cannot inherit one: it follows a text node,
   and a text node has no margin. Setting the line to `display: flex` would give
   it a gap and would also turn "Signed in as" and the address into separate
   flex items with a gap between them, which is the trap `.start__browse` two
   sections down was written to avoid. A sentence is not a set of boxes. */
.acct-meta .role-badge { margin-inline-start: var(--sp-3); }

.watchlist {
  display: grid;
  list-style: none; margin: 0; padding: 0;
  border-block-start: var(--rule-heavy);
}
.watchlist__item {
  display: grid; gap: var(--sp-2);
  padding-block: var(--sp-4);
  border-block-end: var(--rule-thin);
}
.watchlist__item:last-child { border-block-end: 0; }
/* The identity line. Baselines, not centres: the chip is an identifier set
   beside a name and the two sit on one line of type. */
.watchlist__co {
  display: flex; align-items: baseline; gap: var(--sp-3);
  font-size: var(--t-base);
}
.watchlist__name { color: var(--c-ink-strong); }
/* Block-axis halo to the floor: measured 31px drawn, and the acts row sits
   8px below while the halo needs 6.5, so nothing is contested (the person
   cell's lesson, BUG-20260819T1705Z, checked before reusing the mechanism
   it retired there). */
a.watchlist__name { position: relative; }
a.watchlist__name::before {
  content: ""; position: absolute;
  inset-block: min(0px, calc((100% - 2.75rem) / 2));
  inset-inline: 0;
}
.watchlist__acts {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3) var(--sp-5);
}
/* THE PHONE LAYOUT IS DEFINED, NOT INHERITED (WRK-20260819T1401Z). Below
   48rem the subgrid columns stop applying and nothing replaced them: every
   row rendered as chip-and-name, then switch, EMAIL ME and REMOVE clumped on
   the left edge with no column structure, which is what the PI photographed
   as "elements THROWN on the page". The phone shape is the list-row
   convention every settings screen uses: identity on its own line, the
   switch and its label anchored left, the one destructive control ranged
   right, so the row has two anchors instead of one clump and the control
   that removes is never adjacent to the control that mutes. */
@media (width < 48rem) {
  .watchlist__acts { flex-wrap: nowrap; }
  .watchlist__acts .btn-remove { margin-inline-start: auto; }
}
/* The switch acquires a VISIBLE label. In the table the column head said TRACK
   and the stacked view repeated it as a ::before; with the head gone the
   control has to say what it does in view and not only to assistive tech. */
.switch__label {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-muted);
  margin-inline-start: var(--sp-2);
}
@media (min-width: 48rem) {
  .watchlist {
    grid-template-columns: minmax(0, 1fr) max-content max-content;
    column-gap: var(--sp-5);
  }
  .watchlist__item {
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: center; justify-items: start;
  }
  .watchlist__acts { display: contents; }
}

/* -----------------------------------------------------------------------------
   THE QUOTA METER WAS DELETED HERE ON 2026-08-11. `BUG-20260810T0745Z`.

   `.meter`, `.meter__head`, `.meter__track` and `.meter__fill` stood in this
   space. If you are about to reach for a proportion bar, read this first, so
   that you build a working one rather than rebuilding the broken one.

   IT NEVER PAINTED, NOT ONCE. `.meter__fill` set a block-size and a background
   and no `inline-size`, and the dashboard's script wrote the width to a custom
   property, `capFill.style.setProperty("--meter-value", pct + "%")`, that no
   declaration in any stylesheet consumed. A custom property nothing reads is
   inert, so the fill kept `inline-size: auto` inside a block track whose only
   content was an empty span, which resolves to zero. It was empty in all 18
   captures taken on 2026-08-10, three page shapes by three viewports by two
   themes. In dark it was worse than empty: `--c-rule-hair` and `--c-sunken`
   resolve to the same primitive, so the track, its hairline and the surface
   behind it were one color and the component was invisible rather than merely
   blank.

   THE COMMENT THAT STOOD HERE IS WHY IT SURVIVED A YEAR. It said that a gauge
   frozen at a false position is forbidden by law 5, which was a true statement
   of intent above a false description of the code, since a gauge frozen at zero
   is exactly the thing it forbade. The half a reader could check, the head
   stating both numbers in words, was correct, so nothing drew the eye to the
   half the component existed for.

   ITS LAST CALLER WENT WITH `DEC-20260810T0652Z`, which replaced it on the
   dashboard for an unrelated defect. `.meter__head` was `justify-content:
   space-between` across `--measure`, so at 1440 the label and its figure sat
   about 400px apart and read as two unrelated things. The cap is a printed
   sentence in the band's lead now, under law 10.

   MEASURED BEFORE DELETING, 2026-08-11, rather than assumed. No template and no
   Python file renders `meter__head`, `meter__track` or `meter__fill`. That scan
   stripped Jinja, HTML and JS comments before matching and matched a class
   attribute rather than a substring, because the prose explaining this
   component names the very classes the scan looks for. No test asserts these
   rules exist, and `meter` is not in `RETIRED_VOCABULARY` in
   tests/test_prose_vocabulary.py.

   AND THIS COMMENT IS NOW THE ONLY PLACE `--meter-value` APPEARS IN THE CODE.
   `BUG-20260810T0745Z` proves the property is dead by reporting that `grep -rn
   'meter-value' static/ templates/ ebomi/` returns nothing. As of this deletion
   that grep returns three lines, and all three are prose in this block. The
   record is not thereby wrong and the property is not thereby alive. Anyone
   re-checking that claim must strip comments first, which is the same
   discipline `_markup_only` in tests/test_prose_vocabulary.py exists for, and
   the same trap this repository has walked into five times: a scan for a
   forbidden string matches the sentence that forbids it.

   THE `.meter` IN `templates/sg2/instrument.html` IS A DIFFERENT COMPONENT AND
   WAS DELIBERATELY LEFT ALONE. It spells its parts `.meter-track` and
   `.meter-bar` with one hyphen, it is styled by `static/sg2/instrument.css`,
   and that page is a self-contained alternative direction which loads only that
   sheet. The shared bare name is the real hazard here, and deleting this block
   removes a collision that porting that direction into the shipped product
   would otherwise have hit.

   THE BIBLE NEVER DREW THIS ONE, so nothing moved there. Measured the same day:
   `templates/sg2/bible.html` contains no `meter__` and no class attribute
   naming a meter. Its mentions of the word are the ledger's absolute magnitude
   measure, a different and still-unbuilt thing under `WRK-20260731T0612Z`. The
   comment deleted from here claimed the component was kept because the bible
   drew it, and that reason was never true.

   IF YOU DO NEED A PROPORTION BAR, the working rule is `inline-size:
   var(--meter-value, 0%)` on the fill, and it needs a test that renders it at a
   known fraction and asserts the painted width against the track's. The only
   reason this lasted a year is that neither a test nor a screenshot can tell a
   zero-width fill from a fill that is correctly zero.
   -------------------------------------------------------------------------- */

/* The combobox listbox. .combobox and .listbox__option are queried by name in
   dashboard.html's own script, so neither may be renamed. */
.combobox { position: relative; }
.listbox {
  position: absolute; inset-inline: 0; inset-block-start: calc(100% + var(--sp-1));
  z-index: 5; max-block-size: 18rem; overflow-y: auto;
  background: var(--c-surface); border: var(--rule-control);
  display: grid;
}
.listbox__option {
  min-block-size: 2.75rem; display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--t-small); cursor: pointer;
  border-block-end: var(--rule-hair);
}
.listbox__option:last-child { border-block-end: 0; }
.listbox__option:hover { background: var(--c-hover-wash); }
/* THE ARROW-KEY SELECTION IS A SHAPE, NOT A WASH. Both states used to share
   `--c-hover-wash`, which measures 1.02:1 against the rows around it in dark
   and 1.20:1 in light, so the one signal telling a keyboard user which company
   Enter is about to add was invisible in dark and marginal in light.

   Law 2's requirement is that a difference survive greyscale, and a wash at
   1.02:1 does not survive anything. A 3px bar in the mark colour on the inline
   start edge does, and it is the same device the system already uses to open a
   section. The inset keeps the row's text on its existing left edge, so
   nothing shifts as the reader arrows down the list.

   Hover keeps the wash: a pointer is already ON the row it is about to choose,
   so the wash is confirmation rather than the only signal. */
.listbox__option[aria-selected="true"] {
  background: var(--c-hover-wash);
  box-shadow: inset 3px 0 0 0 var(--c-mark);
}

/* The loading placeholder. Law 5 again: a reservation never lies about what it
   holds, so a skeleton line is pinned at the height of the row it stands in
   for and never animates a shimmer that implies progress it is not measuring.
   It is a dotted rule, the system's meaning for not-yet-settled. */
.skeleton { display: grid; gap: var(--sp-3); }
.skeleton--line { block-size: 1.25rem; border-block-end: 1px dotted var(--c-rule); }

/* The table caption. It is a sentence of guidance, so it sets left at the
   body's own size rather than as a centred title: a centred caption above a
   left-aligned ledger reads as a headline for the page rather than as a note
   about the table. */
.table caption {
  caption-side: top; text-align: start;
  font-size: var(--t-mini); color: var(--c-ink-muted);
  padding-block-end: var(--sp-3);
}


/* -----------------------------------------------------------------------------
   12. THE DOCUMENTS THAT LIVE IN PYTHON
   -----------------------------------------------------------------------------
   Five user-facing pages are built as strings in route modules rather than as
   Jinja templates, because each is reached from a token-bearing URL, each sets
   its own security headers, and templates/ belongs to one lane in a repository
   with a single working tree. ebomi/utils/minidoc.py renders them and explains
   why they stay in Python.

   They carry NO SCRIPT, which is a security control on the magic-link
   confirmation (BUG-20260730T2039Z) and is enforced in minidoc.render. So the
   pre-paint theme script cannot run here and these pages follow the operating
   system's prefers-color-scheme rather than a choice made earlier on the site.
   Nothing below may assume otherwise, and in particular nothing may key off
   data-theme, which is never stamped on these documents.

   There is no site header and no footer on purpose. A person is being asked to
   confirm which account they are about to enter, or which address they are
   about to stop; navigation is the one thing that page should not offer.
   -------------------------------------------------------------------------- */
.minidoc {
  padding-block: var(--sp-9) var(--sp-8);
  /* These documents are one column of reading with a single control, so they
     take the reading measure alone rather than the shell's measure-plus-flanks
     track. A confirmation sentence set 1088px wide is a sentence nobody
     finishes. */
  --flank: 0rem;
}
.minidoc .page { max-inline-size: var(--measure); }
.minidoc h1 { font-size: var(--t-title); line-height: var(--lh-head); }
.minidoc p { max-inline-size: var(--measure); }
/* The address a reader is being asked to recognise. It is the whole security
   value of the confirmation page, so it is set in the identifier face at the
   body size and never truncated, wrapped mid-word instead, because an elided
   address defeats the recognition the page exists to obtain. */
.minidoc .addr {
  font-family: var(--face-ident);
  font-weight: var(--w-semi);
  color: var(--c-ink-strong);
  overflow-wrap: anywhere;
  /* An address keeps the case it was written in, even inside a .btn, which
     uppercases its label. Rendered without this the confirmation button read
     CONTINUE AS ADMIN@FIXTURE.INVALID, and an address a reader is being asked
     to recognise must not be shown in a case nobody typed. */
  text-transform: none;
  letter-spacing: normal;
}
.minidoc .btn .addr { color: inherit; }

/* Each unsubscribe move is its own <form>, because each is a separate POST and
   a link would be a GET that changed something. display:contents lets the
   button be the flex item of the surrounding .btn-group, so several one-button
   forms lay out as one row of buttons rather than as a column of forms. */
.unsub-move { display: contents; }


/* -----------------------------------------------------------------------------
   13. SHARING A ROW
   -----------------------------------------------------------------------------
   Bible 13.10. Added 2026-08-10 at the owner's request: every filing on the
   front page gets a copy-link control and a set of share destinations.

   THREE LAWS CONSTRAIN THIS AND EACH REMOVES THE OBVIOUS ANSWER.

   Law 1, the chrome is achromatic, forbids brand-coloured social buttons. The
   destinations are therefore set as words in the furniture face, told apart by
   their label alone, which is also the only treatment that survives greyscale
   and a photocopier.

   Law 11, feedback is printed and nothing expires, forbids the "Copied!" toast
   that every clipboard control ships with. The control states its own outcome
   in place, permanently, and a reader can look away and come back to it. That
   also removes the timer that would otherwise decide how fast someone reads.

   ADR-20260729T0001Z, no external asset host, forbids the vendor SDKs. Every
   destination is a plain anchor to a share URL, which is a LINK rather than an
   asset: nothing is fetched, nothing executes, and no third party learns a
   visitor was here until they choose to go. tests/test_external_assets.py draws
   exactly that distinction, so these do not trip it.

   The icon is inline SVG in currentColor for the same reason: a sprite sheet
   would be an asset, and this way the mark inherits the theme for free.
   -------------------------------------------------------------------------- */
.share {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-3);
}
.share__label {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  color: var(--c-ink-faint);
}

/* The copy control. Icon-only at rest, so it can sit at the end of a dense tape
   row without taking a column, and it carries its accessible name rather than
   relying on the glyph to be self-evident. */
.share__copy {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-inline-size: 2.75rem; min-block-size: 2.75rem;
  padding-inline: var(--sp-2);
  border: 0; background: none; cursor: pointer;
  color: var(--c-ink-muted);
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
}
.share__copy:hover { color: var(--c-ink); }
.share__copy svg { inline-size: 1rem; block-size: 1rem; flex: none; }

/* The outcome, printed into the row. `aria-live="polite"` so it is announced
   once rather than on every keystroke, and it is empty until something has
   happened, so it occupies no space on a page nobody has interacted with. */
.share__said {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  color: var(--c-ink-muted);
}
.share__said:empty { display: none; }

/* Destinations. Words, not marks, and the underline is the hover channel so
   nothing depends on hue (law 2). */
.share__to {
  font-family: var(--face-ui); font-size: var(--t-micro);
  letter-spacing: var(--tr-caps-snug); text-transform: uppercase;
  color: var(--c-ink-muted); text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2);
  /* Both axes. The block floor alone left "X" at 8x44, because a one-character
     label is one character wide, and the 44px rule is 44 SQUARE. Caught by
     tools/ux/targets.py rather than by looking, which is the whole reason that
     probe exists. */
  min-block-size: 2.75rem; min-inline-size: 2.75rem;
}
.share__mark { flex: none; }
.share__to:hover { color: var(--c-ink); text-decoration: underline; text-underline-offset: 0.14em; }

/* On a tape row the cluster is quiet until the row is hovered or something in
   it takes focus, so six rows do not read as twelve controls. It is never
   `display: none`: hiding it from the keyboard would make it unreachable, and
   :focus-within is what keeps it operable without a pointer. Under reduced
   motion there is no transition, only the state change (section 6). */
/* THE SHARE CLUSTER ON A PHONE. The PI, of the front page on his iPhone:
   "Sharing links look off."

   Measured at 390: the cluster is 302px and its five children are
   SHARE 45, COPY LINK 106, X 44, LINKEDIN 59, EMAIL 44. That totals 298 before
   gaps, so EMAIL wrapped alone onto a second line with a wide gap beside it,
   and a lone control under a full row reads as a mistake rather than a line.

   THE LABEL IS WHAT GOES. Four controls reading COPY LINK, X, LINKEDIN and
   EMAIL do not need a word above them saying SHARE: the label is the only
   element in the cluster that is not a control, and dropping it leaves
   253 + 3 gaps = 289 in 302, which fits on one line with room. The accessible
   name is unaffected, because the cluster's controls each carry their own and
   the label was never what named them.

   Same judgment as the "Theme" word in the masthead, and for the same reason:
   on the smallest screen the chrome has to prove it earns its width. */
@media (max-width: 30rem) {
  .share:not(.share--compact) .share__label { display: none; }
  /* TWO ROWS, DEFINED, NOT A RAG. The one-row cluster fit 390 with 13.56px of
     slack at the old type scale; the 2026-08-19 scale move spent it, and
     Email wrapped alone under a full row again, which is the exact shape the
     label-deletion fix was bought to end. A layout that exists at one width
     and rags at another is the watchlist defect in another costume, so the
     phone shape is stated: the copy control on its own row, the three
     destinations aligned on their own. Defined rows also carry the marks
     comfortably (row measured 212px in 287px at 375), so the phone keeps
     mark AND word and decision 2's "unless measurement forces a choice"
     clause is no longer invoked. WRK-20260819T1401Z. */
  .share:not(.share--compact) {
    display: grid;
    grid-template-columns: repeat(3, max-content);
    justify-items: start;
  }
  .share:not(.share--compact) .share__copy,
  .share:not(.share--compact) .share__said { grid-column: 1 / -1; }
  .share:not(.share--compact) .share__said:empty { display: none; }
}

/* AND THE PROVENANCE LINE STOPS BEING A RUN-ON. At 390 it wrapped mid-clause
   three times, so it read "SOURCE 9999999999-00-000000 ·" / "on sec.gov ·
   accepted 2026-08-19 05:02 UTC ·" / "retrieved 11 Aug 2026, 11:00 GMT+10",
   with a separator ending two lines and nothing lining up. A middot is a
   separator between things on ONE line; at the end of a line it is a
   typographical error.

   Stacked instead, one fact per line, and the separators are hidden because
   the line break is now doing their job. This is bible 8.1's argument applied
   to a phone: a provenance strip is metadata, and metadata that has to wrap is
   a list.

   THIS BLOCK ONLY RUNS UNDER 30rem, and for months a comment here implied the
   stranding problem was closed. It was not: from 481px up to each wrap band
   the strip still wrapped and stranded its middots at the row edge, live on
   production at every width except 880-959. The general fix is the stamp: the
   shell script marks any separator whose neighbours sit on different rows,
   and the rule below this block retires it. */
@media (max-width: 30rem) {
  .provenance { display: grid; gap: var(--sp-1); justify-items: start; }
  .provenance > [aria-hidden="true"] { display: none; }
}
.provenance > [aria-hidden][data-stranded] { visibility: hidden; }
/* A fact is atomic. Without this the grid put "accepted" and its date on
   separate rows, which is a label parted from the value it labels: worse than
   the run-on it replaced, and the reason the markup gained a wrapper rather
   than the stylesheet gaining another override. */
.provenance__fact { display: inline; }

.share--compact { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* THE COPY OUTCOME MUST NOT RESIZE A SHARED TRACK. The rule twenty lines below
   already states the principle for the control's own appearance: "a control
   that reflows the row on hover is a control you cannot hit." Printing "Copied"
   into this cluster broke it in a worse way than hover ever could, because
   `.tape-row` adopts `.live-tape__list`'s tracks with `subgrid`, so the text
   widened track 5 for EVERY row at once and the whole tape rearranged on a
   single click. The PI reported it in those words (BUG-20260818T1042Z).

   That is a defect I introduced with the subgrid: before it, the same text
   moved one row. Making the columns line up made a local shift global.

   So on success the announcement is visually hidden and the button swaps its
   glyph, same 16px box, no reflow, and the live region still fires. The BLOCKED
   message stays visible: it tells the reader to select the link instead, which
   is an instruction rather than a confirmation, and a rare reflow is worth it. */
.share__icon--ok { display: none; }
.share--compact [data-share-state="ok"] .share__icon--copy,
.share--compact[data-share-state="ok"] .share__icon--copy { display: none; }
.share--compact[data-share-state="ok"] .share__icon--ok { display: inline; }
.share--compact[data-share-state="ok"] .share__said {
  position: absolute; inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* In a dense row the control is drawn SMALL and reaches its floor with the
   invisible extension, which is the same branch section 8 offers everywhere and
   the same one .tape-row__who a already takes eight lines above.

   Measured: at a 44px drawn box the copy control pushed the tape row from 54px
   to 87px and made it wrap, because the row is `flex-wrap: wrap` and the button
   plus the name plus the value no longer fit on one line. Growing the box to
   meet an accessibility floor and destroying the density it sits in is not
   meeting the floor, it is moving the defect. */
.share--compact .share__copy {
  min-block-size: 0; min-inline-size: 0;
  padding: 0; position: relative;
}
.share--compact .share__copy::before {
  content: ""; position: absolute;
  inset-block: min(0px, calc((100% - 2.75rem) / 2));
  inset-inline: min(0px, calc((100% - 2.75rem) / 2));
}
/* Quiet until the row is engaged. It still occupies its 44px, so nothing moves
   when it appears: a control that reflows the row on hover is a control you
   cannot hit. */
.tape-row .share { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }
.tape-row:hover .share,
.tape-row:focus-within .share { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .tape-row .share { transition: none; } }
/* A pointer that cannot hover never gets the reveal, so the cluster is simply
   always visible there. This is the branch that keeps it usable on a phone. */
@media (hover: none) { .tape-row .share { opacity: 1; } }


/* -----------------------------------------------------------------------------
   THE TURNSTILE ATTRIBUTION, WHICH HAS NO CALLER AND NEVER HAD A REQUIREMENT
   -----------------------------------------------------------------------------
   THIS COMMENT USED TO OPEN "A LICENCE CONDITION WEARING A CAPTION'S CLOTHES",
   and assert that Cloudflare permits hiding the widget only if its privacy
   policy and terms are referenced visibly near the form. That is retracted.
   `BUG-20260811T0002Z`, and the correction is carried in full by the class
   docstring of `TestHidingTheWidgetKeepsItsAttribution`.

   Cloudflare's condition, read rather than recalled, is "as a condition of
   enabling invisible mode, you must reference Cloudflare's Turnstile Privacy
   Addendum in your own privacy policy": a line in a privacy policy, not text
   beside a form. It attaches to invisible MODE, and `interaction-only` is an
   APPEARANCE on a Managed widget, so it was never triggered here at all. The
   paragraph this rule styled linked a Privacy Policy and Terms of USE, which is
   GOOGLE reCAPTCHA's pair rather than the Addendum Cloudflare names.

   THIS COMMENT IS WHY THE CORRECTION IS BEING WRITTEN A THIRD TIME. The template
   and the test were corrected on 2026-08-11 and this block was missed, leaving
   it the last place in live code asserting the rule as fact, in the same firm
   voice everywhere else had recanted. Whoever opens `platform.css` reads this
   and not the test. Found by `-gtm`, who searched for surviving copies on the
   reasoning that a retraction is complete only when every confident uncited copy
   of the claim is gone. That search is the reusable part.

   THE RULE STAYS, AND IT CURRENTLY HAS NO CALLER. No template renders
   `.turnstile-note`. It is kept because removing it belongs with whatever
   settles the privacy policy, which is `-eng`'s, and because
   `test_the_notice_is_styled_rather_than_inheriting_whatever` asserts it exists.
   It is therefore the same shape as `.meter` in `BUG-20260810T0745Z`, a loaded
   gun for the next caller, and it is named as such rather than left to be
   rediscovered.

   DO NOT TREAT THE METER'S DELETION AS THE PRECEDENT FOR DELETING THIS. `.meter`
   was in fact deleted on 2026-08-11 under that record and its tombstone is above
   in this file, so a reader arriving here from that comparison now finds the
   parallel resolved in the direction of deletion. The two cases differ on both
   points that matter. The meter was BROKEN as well as uncalled, and it had no
   pending decision attached to it and no test holding it. This rule is correct
   code waiting on a decision in another lane, and deleting it turns
   `test_the_notice_is_styled_rather_than_inheriting_whatever` red. No caller is
   the shared symptom, not the shared reason.

   The styling below is unchanged and its reasoning still holds for any notice
   that lands here. Set as the quietest thing on the page that is still
   unambiguously readable: the hint size in the furniture face, muted rather than
   faint, because `--c-ink-faint` is for furniture nobody has to read and this is
   text somebody might have to. The links keep their underline; a reference whose
   links are not obviously links is the same defect as no reference. Capped at
   the measure and sitting under the field it describes, so it reads as a
   footnote to the form rather than as a second instruction competing with the
   button.
   -------------------------------------------------------------------------- */
.turnstile-note {
  margin-block-start: var(--sp-2);
  max-inline-size: var(--measure);
  font-family: var(--face-ui);
  font-size: var(--t-mini);
  line-height: var(--lh-ui);
  color: var(--c-ink-muted);
}
.turnstile-note a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.14em;
}
.turnstile-note a:hover { color: var(--c-ink); }
