/* ============================================================================================
   THE TWOTE CONTENT-PAGE TEMPLATE. Layout and content modules, nothing else.

   WHAT THIS FILE IS. A catalogue of named modules for building a content page in the Twote
   language: a report, an essay, an index, a comparison, an investigation. Every material value
   here is a token from /tokens.css (level 1) or web/template/chrome.css (level 2). This file
   defines no colour, no blur, no radius and no token of its own. It is LEVEL 5 in the page-levels
   table in web/template/README.md, and it may never reach up into a level above it.

   Read `README.md` beside this file before using it. It carries the module catalogue, the four
   materials, when each one is correct, and the traps.

   EVERY SELECTOR IS PREFIXED `ct-`. Not tidiness: a page that borrows /styles.css borrows all
   1,300 lines of it, including `main { display: grid; grid-template-columns: minmax(0, 2.4fr)
   minmax(0, 0.92fr) }` above 1024px. That collision shipped once, on 2026-07-28, and turned a
   five-band report into two columns. The landmark is `<main id="main" class="ct-main">`, where
   `.ct-main` (below) resets that inherited grid back to a full-width block; the cascade uses no `.panel`.

   THE FOUR MATERIALS, in order of how much they lift off the photograph:

     BARE      nothing at all. The photograph. Headings, the opening, a pull quote.
     SHEET     frosted, `.glass`. The only real blur on the page, and it never nests. Prose.
     CARD      the shared surface-on-photograph recipe (CANON 23): --scrim-hard, then --tint-rest as
               a gradient layer, then the lift shadow. A repeated item, or a single raised band.
     WELL      the deep fill (CANON 23's --dark). Data that must stay legible over any backdrop.

   These are CANON 23's four materials under this template's names. A page may not invent a fifth.

   The rule that decides which: PROSE goes in a sheet, DATA goes in a well, ITEMS go in cards,
   HEADINGS go bare. A page that alternates them shows the photograph between every band, which is
   the CANON 14 test for how many panels is too many.

   THERE IS NO LOAD-TIME MOTION IN THIS TEMPLATE, and that is a decision. See the README.
   ============================================================================================ */

/* ============================================================================================
   SHELL
   ============================================================================================ */

.ct-page {
  max-width: var(--ct-wide);
  margin: 0 auto;
  /* A tight top: the opening is the first viewport's content, so the page must not spend half
     of it on air above the mark (Paul, 2026-08-09). */
  padding: 14px 20px 84px;

  /* ONE MEASURE for every text column on the page: the standfirst, each section's intro, and the
     prose inside a module. Change it here and they all move together, which is the only way "the
     same width" survives an edit. */
  --ct-measure: 660px;

  /* THE COMPOSITION SYSTEM: ONE RAIL, TWO SURFACE WIDTHS (2026-08-10, replacing the rejected
     full-width-surface layout: 660px prose hard-left inside 1180px sheets left a dead right
     third on every surface).

     Every surface centres on the page axis. Every running-text block caps at --ct-measure and
     centres too (`margin-inline: auto`). Because centring composes, all text on the page shares
     ONE left edge, the rail, whatever surface or padding it sits inside; nothing needs to know
     its nesting to line up.

     The two widths a surface may take, and the default is chosen by material:
       COLUMN  --ct-col: the measure plus the sheet-and-band inset, so a prose surface hugs its
               text with no dead flank. Sheets take it: prose goes in a sheet.
       WIDE    --ct-wide: the full page. Wells, grids and bare figure sets take it: data earns
               width. Prose inside a wide surface still caps at the measure and centres, so it
               stays on the rail and the flanks stay symmetric.
     Modifiers move a surface off its default (`ct-sheet--wide` for a sheet whose bands hold
     splits or tables; `ct-well--col` for a small aside well) and nothing else may. */
  --ct-col: calc(var(--ct-measure) + 72px);
  --ct-wide: 1080px;

  /* THE TYPE SCALE, one ladder for the whole template (Paul, 2026-08-09: "4 different font sizes
     in half a screen"). Eight steps and nothing between them. Every rule below sizes from one of
     these; a raw px font-size in this file is a bug, not a nuance. The steps are far enough apart
     to read as different roles (44 / 26 / 23 / 17 / 16 / 14.5 / 13 / 12) instead of as drift
     (the 26 is the pull quote's step, its italic moment, added 2026-08-10 to retire the
     off-ladder 30px clamp), and the
     display step tops out well under --wm-mark (84px at desktop) so the lockup stays the largest
     thing on the page. */
  --ct-t-display: clamp(30px, 3.4vw, 44px);
  --ct-t-quote: clamp(21px, 2.5vw, 26px);
  --ct-t-h2: clamp(20px, 2.2vw, 23px);
  --ct-t-h3: 17px;
  --ct-t-lead: 16px;
  --ct-t-body: 14.5px;
  --ct-t-small: 13px;
  --ct-t-micro: 12px;
  /* ROLE ALIASES onto the eight-step ladder (SNAPPED 2026-08-22, Paul; DESIGN.md "The type
     ladder"). These five named roles used to hold off-step sizes; they now alias the nearest ladder
     step so a selector still says what it is while its value stays on the ladder. No role holds a
     value off the ladder.
       --ct-t-caption   figcaption, table name-sub, check dl        = small (13)
       --ct-t-note      check paragraph, card-body paragraph        = small (13)
       --ct-t-cardlead  check-card heading, feature-card lead para  = body  (14.5)
       --ct-t-cardtitle grid card heading                          = h2    (23)
       --ct-t-feature   feature card heading                       = quote (26) */
  --ct-t-caption: 13px;
  --ct-t-note: 13px;
  --ct-t-cardlead: 14.5px;
  --ct-t-cardtitle: 23px;
  --ct-t-feature: 26px;

  /* The width at which two columns of body text stop being two columns. Below this a split module
     stacks. It is a variable so a page with a denser grid can raise it without editing rules. */
  --ct-split-at: 1000px;
}

/* Anything bare on the photograph carries its own shadow: there is no glass under it doing the
   legibility work, and the backdrop has bright patches. */
.ct-onphoto { text-shadow: 0 1px 12px rgba(15, 24, 20, 0.9); }

/* /styles.css carries `a:hover { text-decoration: underline }` at specificity 0,1,1, which beats a
   plain class at 0,1,0. text-decoration propagates to descendants, so wrapping a whole block in an
   anchor underlines every line inside it on hover. These are 0,2,0 or higher so they win, and the
   hover state moves to the one element whose job is to say "this is a link". */
.ct-card:hover,
.ct-card:focus-visible,
.brand-home:hover,
.brand-home:focus-visible { text-decoration: none; }

/* SKIP LINK (P2 landmarks, 2026-08-22): the first body child on every article page. Off-screen
   until focused, then it drops in as the template glass sheet. No local focus ring: styles.css's
   global a:focus-visible carries the green ring. */
.ct-skip {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 1000;
  padding: 10px 16px;
  border-radius: var(--radius-inner);
  background-color: var(--glass-fill);
  background-image: var(--glass-sheen);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  color: var(--text);
  font-size: var(--ct-t-small);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(calc(-100% - 24px));
}
.ct-skip:focus { transform: none; }

/* MAIN LANDMARK RESET (P0, 2026-08-22): the article landmark is `<main id="main" class="ct-main">`.
   The bare `main` element is styled by /styles.css as the feed's two-column glass grid (display:grid,
   grid-template-columns:2.4fr/0.92fr and a capped height above 1024px), which rendered every article
   as a broken two-column layout with empty flanks. This class neutralises that inheritance so the
   landmark is an ordinary full-width block. */
.ct-main {
  display: block;
  height: auto;
  grid-template-columns: none;
  gap: 0;
  align-items: normal;
}

/* SR-ONLY: exposed to assistive tech, off the visual page. Carries the index's landmark heading
   so the heading order reads h1 > h2 > h3 without a visible second title. */
.ct-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- THE SHARED SHELL'S STICKY CHROME (CANON 23, 2026-08-09 amendment) ----
   One bar per page, OUTSIDE .ct-page so it spans the viewport. MATERIAL, 2026-08-23 ruling (Paul's
   pick from the header variants, recorded by the supervisor as a CANON 14 amendment): the
   content-page chrome bar takes the FROSTED recipe so the forest reads through it, overriding
   `.sticky-chrome`'s deep fill. The deep fill stays as the no-blur fallback below, which is the
   legibility case CANON 14 gave it. The mark inside it is the CHROME role, which is the lockup block's default
   `--wmSize`, so no size is written here (CANON 16). The right-hand slot takes a `.ct-back`
   section link on a page that has somewhere to go back to, or a `.ct-chrome-note` on one that
   does not.

   IT APPEARS ONCE THE MASTHEAD SCROLLS AWAY. A tiny inline script (the shell script, copied
   beside this markup on every page) watches the masthead with an IntersectionObserver and holds
   `is-tucked` on the bar while the masthead is in view. THE MARKUP SHIPS THE BAR ALREADY
   `is-tucked` (2026-08-10, the first-paint fix): the script runs at end of body, so an untucked
   default painted the chrome lockup over the masthead lockup for a frame at scroll 0. Starting
   tucked makes the first paint correct and the observer takes over from there. A `<noscript>`
   style inside the chrome block restores the no-JS resting state (bar simply visible from the
   start, an acceptable permanent state); it matches `.ct-chrome.is-tucked` at equal specificity
   and wins by document order. This is interaction motion with correct resting states, not
   load-time motion (see the README). It is NOT a CSS scroll-driven
   animation, deliberately: /styles.css makes `body` a scroll container that never scrolls (the
   viewport does the scrolling), so `animation-timeline: scroll()` resolves to an inactive
   timeline and the bar never hides; measured 2026-08-09 in Chromium with `getAnimations()`
   reporting `currentTime: null`. `visibility` rides the transition so the tucked bar cannot
   take clicks or focus. */
.ct-chrome {
  /* FIXED, deliberately overriding .sticky-chrome's `position: sticky` (the contract that class
     carries is the deep fill and the no-stacking rule, both kept). Sticky keeps its flow box, so
     the tucked bar was still spending ~70px of air above the masthead: half the first viewport's
     opening was margin (Paul, 2026-08-09). Fixed takes it out of flow entirely. */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  transition-property: opacity, translate, visibility;
  transition-duration: 280ms;
  transition-timing-function: var(--ease-out);
}
/* FROSTED SURFACE. Written at `.ct-chrome.sticky-chrome` (0,2,0) so it beats `.sticky-chrome`'s
   `background: var(--glass-fill-deep)` on SPECIFICITY and stays correct whichever order the two
   stylesheets load in. The `background` shorthand it replaces also cleared the sheen, so both
   layers are restated here. Frosted sheets (`.ct-sheet.glass`) do pass beneath it while the
   page scrolls; that is the trade Paul chose with V2 (CANON 14, 2026-08-23 amendment), and the
   CANON 14 rule that stands is that no SECOND sticky bar stacks on this one. */
.ct-chrome.sticky-chrome {
  background-color: var(--glass-fill);
  background-image: var(--glass-sheen);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
}
/* No backdrop-filter, no frost: fall back to the deep fill, since a 0.18 tint with nothing blurred
   behind it puts scrolling body text straight through the bar. Set on the properties, inside this
   @supports block after the frosted rule at equal specificity, so order carries it; `--glass-fill`
   itself is never redefined outside the template block (web/layers.test.js guards the name). */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ct-chrome.sticky-chrome { background-color: var(--glass-fill-deep); background-image: none; }
}
.ct-chrome.is-tucked {
  opacity: 0;
  visibility: hidden;
  translate: 0 -100%;
}
.ct-chrome-inner {
  /* Matches .ct-page's --ct-wide (this rule sits outside .ct-page, so the var is out of scope). */
  max-width: 1080px;
  margin: 0 auto;
  padding: 4px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px 24px;
}
.ct-chrome-note { font-size: var(--ct-t-small); color: var(--textQuiet); }
/* On the frosted bar the quiet text measured 4.3:1 over bright index artwork (session-158 review
   F1), so the note takes the full text colour there. */
.ct-chrome.sticky-chrome .ct-chrome-note { color: var(--text); }
/* The overlay nav drops the tagline row: it is a slim reveal, not a second masthead, so the mark
   alone carries it and the bar crops to one line (Paul, 2026-08-15). The masthead keeps its
   tagline. Hiding it here leaves the pinned lockup markup and its CANON 16 CSS block untouched. */
.ct-chrome .wordmark-tagline { display: none; }

/* ---- Masthead: the mark and the page's identity, bare ---- */
/* The masthead lockup and the sticky chrome bar carry the SAME CHROME role, so the mark is one
   size across both (Paul, 2026-08-15: the MARK-size masthead read as ridiculously large; it now
   matches the overlay nav). Neither pulses: nothing on a content page is live (CANON 16). */
.ct-masthead .wordmark-lockup { --wmSize: var(--wm-chrome); }
.ct-masthead {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px 24px;
  flex-wrap: wrap;
  margin-bottom: 26px;
}
.ct-masthead-meta {
  text-align: right;
  margin: 0;
  font-size: var(--ct-t-small);
  line-height: 1.5;
  color: var(--textQuiet);
  text-shadow: 0 1px 12px rgba(15, 24, 20, 0.9);
}
.ct-masthead-meta b {
  /* Same size as the rest of the meta, deliberately: the bold weight and the brighter ink do the
     emphasis. A 16px step here was one more size competing in the first viewport. */
  display: block;
  font-size: inherit;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}
.ct-back {
  color: var(--textQuiet);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition-property: color, border-color;
  transition-duration: var(--dur-tint);
  transition-timing-function: var(--ease-out);
}
.ct-back:hover { color: var(--text); border-bottom-color: var(--hairline); }

/* ---- The opening: title, standfirst, dateline. Bare, so the page starts on the photograph ---- */
.ct-title {
  /* ON THE RAIL: the title's box is the measure, centred, so its left edge is the same left edge
     every paragraph on the page has. `balance` does the line-length work 20ch used to do. */
  margin: 0 auto 18px;
  max-width: var(--ct-measure);
  /* AN EDITORIAL HEADLINE, NOT A POSTER (Paul, 2026-08-09, rejecting 58px): the display step of
     the scale above, 44px at desktop, half the mark's 84px. At 58 the title was the second-largest
     thing on the page by a hair and the opening read as three competing display sizes. */
  font-size: var(--ct-t-display);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
/* THE DEK: one line under the title, when a page wants a second thought in the masthead voice
   rather than in the standfirst's. It is a heading step, not a display one, so the opening reads
   title then dek then standfirst and never as two competing headlines. Requested furniture only:
   a page without a dek does not grow one. */
.ct-dek {
  margin: -6px auto 18px;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-h2);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--textQuiet);
  text-wrap: balance;
}
.ct-standfirst {
  margin: 0 auto 16px;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-lead);
  line-height: 1.62;
  color: var(--textQuiet);
  text-wrap: pretty;
}
.ct-standfirst strong { color: var(--text); font-weight: 500; }
.ct-dateline {
  margin: 26px auto 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-small);
  letter-spacing: 0.02em;
  color: var(--textQuiet);
}

/* ---- Section rhythm. The heading sits BARE and its surface follows underneath, which is what
       breaks the backdrop up: the reader crosses open photograph between every band. ---- */
.ct-section { margin-top: 52px; }
/* Deep-link landing clears the revealed sticky nav (65px): a section reached by #id would otherwise
   sit under the bar (Paul, 2026-08-15). One rule covers every anchor, since every anchored target is
   a .ct-section. Keep this a little above the bar height so the heading lands with air, not flush. */
.ct-section[id] { scroll-margin-top: 80px; }
.ct-section:first-of-type { margin-top: 46px; }
/* Panel to panel, the gap is a beat rather than a void (Paul, 2026-08-27). A section carrying no bare
   heading IS a self-padded surface: the watch panel, the coming-soon strip, a report drop. It and the
   surface above it both hold their own padding and their own shadow, so the full section step above
   that reads as a sparse page. Half the step, which stays clearly a section break and stays clear of
   the 16px within-section seam below. */
.ct-section:not(:first-of-type):not(:has(> .ct-sectionhead)) { margin-top: 26px; }
.ct-sectionhead { margin-bottom: 18px; }
.ct-sectionhead h2 {
  margin: 0 auto 8px;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-h2);
  font-weight: 600;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
.ct-sectionhead p {
  margin: 0 auto;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-body);
  line-height: 1.6;
  color: var(--textQuiet);
  text-wrap: pretty;
}
/* A second surface inside one section sits close to the first, so the two read as one group with a
   seam rather than as two sections with a missing heading. */
.ct-section > * + .ct-sheet,
.ct-section > * + .ct-well,
.ct-section > * + .ct-grid { margin-top: 16px; }

/* ============================================================================================
   SURFACES
   ============================================================================================ */

/* SHEET: frosted, and the only real blur on the page. `.glass` comes from chrome.css and carries
   the recipe; this adds padding and the COLUMN width: prose goes in a sheet, so a sheet hugs its
   prose instead of carrying a dead flank of glass beside it. A sheet whose bands genuinely need
   room (a split, a table, a run of wide entries) says so with `ct-sheet--wide`. */
.ct-sheet {
  padding: var(--pad-panel);
  max-width: var(--ct-col);
  margin-inline: auto;
}
.ct-sheet--wide { max-width: none; }

/* WELL: the dark step, and it takes THE DEEP FILL rather than a scrim of its own.

   This was built first as `--scrim-hard` alone, which looked right and was a fifth surface. CANON 23
   landed the same day from the Oddities work and names four materials and one shared recipe, with
   `--glass-fill-deep` as the dark step for a band that must stay legible over any backdrop. That is
   exactly what a table of fine figures is: every translucent step is a gamble against a photograph
   the author never sees, and there are six of them. Nothing translucent is stacked over this, which
   is the condition CANON 14 attaches to the deep fill. */
.ct-well {
  padding: var(--pad-panel);
  border-radius: var(--radius-panel);
  background: var(--glass-fill-deep);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 1px 2px rgba(0, 0, 0, 0.28),
    0 10px 28px rgba(0, 0, 0, 0.30);
}
/* A well is WIDE by default (data earns width); a small aside well can come back to the column. */
.ct-well--col {
  max-width: var(--ct-col);
  margin-inline: auto;
}

/* CARD: the middle step. THE HARD SCRIM FIRST, then the white tint. A card carries body text over
   a SHARP photograph with no blur under it: at --scrim-soft the copy washes out against a lit tree
   trunk. CANON 18 states the rule, that a no-blur surface over a sharp photograph needs the dark
   step first and the white tint only does the raised work. */
.ct-card {
  display: block;
  border-radius: var(--radius-panel);
  background-color: var(--scrim-hard);
  background-image: linear-gradient(var(--tint-rest), var(--tint-rest));
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 1px 2px rgba(0, 0, 0, 0.28),
    0 10px 28px rgba(0, 0, 0, 0.30);
  overflow: hidden;
}
/* Only a card that IS a link gets link behaviour. A card used as a container does not pretend. */
a.ct-card {
  text-decoration: none;
  color: inherit;
  transition-property: background-color, box-shadow, scale;
  transition-duration: 180ms;
  transition-timing-function: var(--ease-out);
}
a.ct-card:hover {
  background-color: rgba(0, 0, 0, 0.66);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.09),
    0 2px 4px rgba(0, 0, 0, 0.30),
    0 16px 40px rgba(0, 0, 0, 0.38);
}
a.ct-card:active { scale: 0.985; }

/* FROST: the card material read as clickable, not decorated. Paul, 2026-09-01, session 204: the
   onward/explainer CTA cards on the VS crossover pages read as dark forest panels rather than
   controls, so this modifier swaps the card's dark scrim for CANON 14's own FROSTED recipe, the
   identical tokens `.glass` uses on a sheet (`--glass-fill` + `--glass-sheen` + `--glass-blur` +
   `--glass-shadow`), never a new blur or a new colour. Apply only where the card is NOT already
   nested inside another frosted panel (CANON 14 NEVER NEST): these are onward cards sitting bare
   on the backdrop or beside a sheet, never inside one. Declared after .ct-card's own hover/active
   rules so it wins the cascade at equal specificity by source order. */
.ct-card--frost {
  background-color: var(--glass-fill);
  background-image: var(--glass-sheen);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}
a.ct-card--frost:hover,
a.ct-card--frost:focus-visible {
  background-color: var(--glass-fill);
  background-image: var(--glass-sheen);
  box-shadow: var(--glass-shadow), inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}

/* BAND: a hairline-divided compartment inside a sheet or a well. This is how one surface holds
   several subjects without becoming several surfaces.

   IT IS ALSO THE QUERY CONTAINER, and that is what makes these modules portable. Every module that
   changes shape does it on the width of the BAND it is sitting in, never on the width of the window.
   A table in a narrow column stacks on a wide monitor, because the thing that decides is how much
   room it actually has. Viewport media queries cannot express that, and a module that only works at
   full page width is not a module. */
.ct-band,
.ct-entry {
  container-type: inline-size;
  container-name: ct-band;
}
.ct-band {
  padding: 24px 20px;
  border-top: 1px solid var(--hairline);
}
.ct-band:first-child { border-top: 0; }
.ct-band > h2,
.ct-band > h3 {
  margin: 0 auto 6px;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-h3);
  font-weight: 600;
  letter-spacing: -0.012em;
  text-wrap: balance;
}
.ct-band > h2 + p,
.ct-band > h3 + p {
  margin: 0 auto 20px;
  /* ONE MEASURE, not a ch count. A ch value is a different pixel width at every font size, which
     is what put four text blocks on four different right edges (Paul, 2026-08-09: "randomly left
     aligned content text throughout"). Every running-text block in this file uses --ct-measure. */
  max-width: var(--ct-measure);
  font-size: var(--ct-t-body);
  line-height: 1.6;
  color: var(--textQuiet);
  text-wrap: pretty;
}

/* THE FLOOR IS THE PADDING (Paul, 2026-08-10: "spacing at the bottom of these modules").
   The last block inside a surface or band drops its bottom margin, so every module ends at the
   same inset instead of at padding plus whatever margin its last module happened to carry. */
.ct-band > :last-child,
.ct-sheet > :last-child,
.ct-well > :last-child,
.ct-entry > :last-child { margin-bottom: 0; }

/* The two inner steps, for a block that must read as raised INSIDE a surface without a second
   blur. Concentric: a 28px surface with 16px padding gives 12px inside. */
.ct-inset,
.ct-panel {
  padding: 15px 17px;
  border-radius: var(--radius-inner);
}
.ct-inset { background: var(--tint-recess); }
.ct-panel { background: var(--tint-rest); }

/* ============================================================================================
   TEXT
   ============================================================================================ */

.ct-prose {
  margin: 0 auto 12px;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-body);
  line-height: 1.65;
  color: var(--textQuiet);
  text-wrap: pretty;
}
.ct-prose:last-child { margin-bottom: 0; }
.ct-prose b { color: var(--text); font-weight: 600; }
.ct-prose--lead { font-size: var(--ct-t-lead); color: var(--text); }

/* Inside the prose column: a quotation set beside the sentence that introduces it (unlike ct-quote,
   which stands alone between surfaces and appears once a page), a prose subhead and a prose list.
   Promoted from the-model-that-hears-less's page-local hl-quote, hl-sub and hl-list (2026-10-12). */
.ct-prose-quote { margin: 4px 0 18px; padding: 0; font-size: var(--ct-t-lead); font-style: italic; line-height: 1.5; color: var(--text); text-wrap: pretty; }
.ct-prose-sub { margin: 26px 0 8px; font-size: var(--ct-t-h3); font-weight: 600; color: var(--text); }
.ct-prose-list { margin: 0; padding-left: 1.2em; }
.ct-prose-list li { margin: 0 0 8px; }

/* A pull quote is BARE, between surfaces, set in Outfit italic (the serif ban of 2026-08-20
   stands for prose; Fraunces italic is allowed for species names only, CANON 16 amendment 2026-09-07). The template's one decorative typographic move, spent on a sentence worth stopping for. */
.ct-quote {
  margin: 34px auto;
  max-width: var(--ct-measure);
  font-family: var(--fontUI);
  font-size: var(--ct-t-quote);
  font-style: italic;
  font-weight: 400;
  line-height: 1.22;
  letter-spacing: -0.015em;
  text-wrap: balance;
  text-shadow: 0 1px 12px rgba(15, 24, 20, 0.9);
}
.ct-quote cite {
  display: block;
  margin-top: 12px;
  font-family: var(--fontUI);
  font-style: normal;
  font-size: var(--ct-t-small);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--textQuiet);
}

/* SPLIT: two columns of body text, for a pair that belongs together. The canonical use is a claim
   beside its caveat, which also makes the caveat the same size as the claim.

   ONE COLUMN IS THE DEFAULT and two is the enhancement, in that order deliberately: if container
   queries are not available the module degrades to a single readable column instead of to two
   30-character ones, which is not a layout but a hazard. Below the threshold it stacks in SOURCE
   order, so the left paragraph has to be the one that reads first. */
.ct-split { display: block; }
@container ct-band (min-width: 860px) {
  .ct-split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 40px;
    align-items: start;
  }
  .ct-split > .ct-prose { max-width: none; margin-bottom: 0; }
  .ct-split > .ct-full { grid-column: 1 / -1; }
}

/* NOTES: multi-column small print. Method, caveats, what was not checked. Columns rather than a
   grid because the items are a list of paragraphs of unequal length and the reader is scanning. */
.ct-notes {
  columns: 1;
  column-gap: 36px;
  font-size: var(--ct-t-small);
  line-height: 1.65;
  color: var(--textQuiet);
}
/* Two columns, never three: at a third column the measure drops under 40 characters and a reader
   starts hopping between lines instead of reading down one. */
@container ct-band (min-width: 700px) {
  .ct-notes { columns: 2; }
}
.ct-notes p { margin: 0 0 13px; break-inside: avoid; text-wrap: pretty; }
.ct-notes p:last-child { margin-bottom: 0; }
.ct-notes b { color: var(--text); font-weight: 600; }

/* LINKS: the sources line at the foot of a module. Its affordance is the underline, so the
   surrounding block can be plain text without competing. NO FOCUS RULE HERE: /styles.css carries a
   global `:focus-visible` ring in the same green, and a local copy at a different offset would be a
   second definition of something level 4 owns. */
.ct-links {
  margin: 16px auto 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-micro);
  color: var(--textQuiet);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  align-items: baseline;
}
.ct-links b { font-weight: 500; }
.ct-links a {
  color: var(--swiftGreenBright);
  text-decoration: none;
  border-bottom: 1px solid rgba(16, 185, 129, 0.3);
  padding-bottom: 1px;
  transition-property: border-color, color;
  transition-duration: var(--dur-tint);
  transition-timing-function: var(--ease-out);
}
.ct-links a:hover { border-bottom-color: var(--swiftGreenBright); }

/* ============================================================================================
   LABELS AND MARKS
   ============================================================================================ */

/* THE CHIP SITS AT --tint-rest, NOT --tint-recess. Built at 0.03 it is invisible over the frosted
   panel and reads as a line of grey text adrift at the end of a heading: the pill carries no shape
   at all. This repo has hit the same class of defect before, with the hairline token drawn over a
   saturated gradient. */
/* A CHIP WRAPS. It was `white-space: nowrap` on the theory that a badge is one short word, and the
   moment one carried a phrase it became a 247px unbreakable box that pushed a 280px screen sideways
   by 9px. A badge that has outgrown one line should take two rather than take the layout with it. */
/* ZERO LINE-BOX IMPACT, BY CONSTRUCTION (Paul, 2026-08-09, rejecting the slimming pass that came
   before it: "the pill should not impact the spacing of the line below"). `display: inline-block`
   was the defect. An inline-block is an atomic box, so the line it lands in grows to its full
   margin box no matter how small the padding gets: at 2px padding the key's pill still measured
   21.55px in a 20px line and pushed the row apart. `display: inline` is the fix and it is
   absolute, not a tuning: an inline non-replaced box contributes only its font metrics to the line
   box, so vertical padding paints without moving anything. The padding is in em so ONE rule gives
   the same optical spread at every size the chip and pill appear at.
   Inside a flex or grid container (a card kicker, an entry head) the display value blockifies to
   `block`, which is what a standalone badge wants, so one declaration covers both uses. */
.ct-chip {
  display: inline;
  max-width: 100%;
  padding: 0.14em 0.62em;
  border-radius: 999px;
  background: var(--tint-rest);
  box-shadow: inset 0 0 0 1px var(--hairline);
  font-size: var(--ct-t-micro);
  font-weight: 500;
  color: var(--textQuiet);
  overflow-wrap: break-word;
}

/* A MARK is a judgement with a dot beside it. THE WORD CARRIES THE MEANING and the colour only
   agrees with it: nothing in this template is legible by colour alone. */
.ct-mark {
  white-space: nowrap;
  font-weight: 500;
}
.ct-mark::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-right: 7px;
  vertical-align: 1px;
  background: currentColor;
}
.ct-mark--good { color: var(--swiftGreenBright); }
.ct-mark--caution { color: var(--caution); }
.ct-mark--neutral { color: var(--fog); }

/* A PILL is a score. Its colour is written by /ramp.js from the RAW value, per CANON 15, and a hex
   written here would be a fourth place for the ramp to drift. */
.ct-pill {
  /* Inline, for the reason written above the chip: a pill appears inside a sentence and may not
     move the line it lands in. It was inline-flex, which is atomic and did. Its content is a
     number and at most one word, so it never wants to wrap. ONE treatment, everywhere a pill
     appears: no page and no section may respell its size or its padding. */
  display: inline;
  white-space: nowrap;
  padding: 0.14em 0.55em;
  border-radius: 999px;
  font-size: var(--ct-t-small);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* STEPS: a discrete scale with this item's step lit, so a reader sees what the word is measured
   against instead of taking it on trust. */
.ct-steps {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin: 20px 0 0;
  padding: 0;
  list-style: none;
}
.ct-steps li {
  padding: 5px 12px;
  border-radius: 999px;
  font-size: var(--ct-t-micro);
  font-weight: 500;
  background: var(--tint-recess);
  color: var(--textQuiet);
}
.ct-steps li[aria-current] {
  background: rgba(16, 185, 129, 0.16);
  color: var(--swiftGreenBright);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.35);
}

/* ============================================================================================
   FIGURES AND DATA
   ============================================================================================ */

/* STATS: a strip of labelled figures. Auto-fit rather than a fixed count, so four facts do not
   wrap 2 / 1 / 1 and read as a ragged list.
   AT MOST FOUR FACTS PER STRIP (Paul, 2026-08-10, after a fifth item wrapped alone onto a second
   row): pick the facts that matter and never pad the strip to fill a track. Two or three are
   fine; auto-fit collapses the empty tracks and spreads the survivors evenly. */
.ct-stats {
  margin: 0 0 18px;
  padding: 14px 16px;
  border-radius: var(--radius-inner);
  background: var(--tint-recess);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(148px, 100%), 1fr));
  gap: 14px 22px;
}
.ct-stats > div { min-width: 0; }
.ct-stats dt {
  font-size: var(--ct-t-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--textQuiet);
  margin-bottom: 4px;
}
.ct-stats dd {
  margin: 0;
  font-size: var(--ct-t-body);
  font-weight: 600;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.ct-stats dd small {
  display: block;
  margin-top: 2px;
  font-size: var(--ct-t-micro);
  font-weight: 400;
  font-variant-numeric: normal;
  color: var(--textQuiet);
}
/* On a well the recess step has nothing to lift off, so the strip is drawn with rules instead. */
.ct-well .ct-stats { background: none; padding: 0; border-top: 1px solid var(--hairline); padding-top: 16px; }
/* The rule separates the strip from what is above it, so a strip with nothing above it in its band
   drops the rule and its 16px: on the species pages' bird section it drew an empty band with a line
   across it over the facts (2026-10-13). */
.ct-well .ct-stats:first-child { border-top: 0; padding-top: 0; }

/* FACTS: the same idea inline, for a short row under a heading rather than a block of its own. */
.ct-facts {
  /* A BOTTOM MARGIN, because prose follows this row on every subject block and the two were
     touching: the last label sat one leading away from the first line of the paragraph and the
     row read as part of it (seen at 1440 on the morepork page, 2026-08-09). */
  margin: 20px auto 18px;
  max-width: var(--ct-measure);

  /* TWO COLUMNS ON A SHARED LEFT EDGE, AT EVERY WIDTH. This was a wrapping flex row, so the number
     of items that happened to fit a line decided where every label after it started: nine facts
     began at nine different horizontal positions and the block read as debris instead of a set.
     A grid gives column one and column two each a fixed left edge and the eye can run down either.
     (Paul, 2026-08-23, on the model-profiles page.) */
  display: grid;
  /* AT MOST TWO COLUMNS, AND ONE AS SOON AS A COLUMN WOULD FALL UNDER 180px. Every track is at
     least half the row, so a third can never fit; below about 394px of room two cannot fit either
     and the set goes single file with the same left edge. Written as a width rule rather than a
     container query because a fact row is not always inside one: on several live articles it sits
     straight in a sheet, where a container query would be dead. */
  --ct-facts-gap: 34px;
  grid-template-columns: repeat(auto-fit, minmax(max(160px, (100% - var(--ct-facts-gap)) / 2), 1fr));
  gap: 18px var(--ct-facts-gap);
  align-items: start;

  font-size: var(--ct-t-small);
  color: var(--textQuiet);
}
/* THREE ACROSS WHERE THREE ARE THE HEADLINE (Paul, 2026-08-24, on the origami article's opening
   row). The two-column default above is for a long fact set inside a sheet, where a fixed left
   edge per column is what makes nine facts readable. An article's opening row is three short
   chips and belongs on one line, and it stays on one line all the way down to a phone. */
.ct-facts--row {
  /* ONE ROW AT EVERY WIDTH, ON EVERY PLATFORM, AND THAT IS THE WHOLE JOB OF THIS MODIFIER
     (Paul, 2026-08-26). What it defends: a three chip opening strip reads as one set of facts,
     and the moment it breaks to two rows the third chip looks like an afterthought hanging under
     the first. The auto-fit arithmetic that used to sit here could not hold it: every track
     carried a 160px floor, three of those plus two gaps need 548px, and a phone leaves about
     358px inside the page gutters, so auto-fit dropped to two tracks and then one. Column
     auto-flow puts every child on one implicit row whatever the count, and minmax(0, 1fr) lets a
     chip's label wrap inside its own track instead of forcing the track wider than the row. The
     gap comes down with the viewport so a phone spends its width on words rather than air.
     If you are tempted to restore a track floor here, that floor is the bug. */
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  /* The gap is the full 34px from 768px up, so a tablet and a desktop render exactly as they did
     before this rule changed, and it closes to 14px on a phone, where the room it costs is room
     the labels need. */
  --ct-facts-gap: clamp(14px, calc(8.7vw - 32.8px), 34px);
}
/* A long unbroken token in a chip label would still push its track wide; wrapping inside the word
   is the last line of defence for the one row rule above. */
.ct-facts--row > * { overflow-wrap: break-word; }
.ct-facts:last-child { margin-bottom: 0; }
.ct-facts b {
  display: block;
  font-size: var(--ct-t-lead);
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* ---- THE TABLE ----
   IT DOES NOT SCROLL SIDEWAYS ON A PHONE, AND THAT IS THE WHOLE POINT OF THIS MODULE.
   The first build put a 908px table in a 272px column inside `overflow-x: auto`. Nothing overflowed
   the page, so an automated check passed it, and it was still wrong: on a phone a sideways swipe
   anywhere over the table drags the table, which reads as the page sliding around, and 272px of a
   908px table is not a table anyone can use. Below the stack width every row becomes a labelled
   block and the scroller is gone.

   THE HEADER ROW IS MOVED OFF-SCREEN RATHER THAN HIDDEN. `display: none` would take it out of the
   accessibility tree and break the association between each cell and its column. Off-screen keeps
   it, the explicit ARIA roles on the markup keep the table semantics once `display` changes, and
   the visible labels come from `data-label` as generated content, which is decoration on top of a
   structure that is still correct underneath. */
.ct-data {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ct-t-small);

  /* STACKED IS THE DEFAULT AND THE TABLE IS THE ENHANCEMENT. Written the other way round, a browser
     without container queries gets the wide table and the sideways scroller, which is the failure
     this module exists to remove. This way the worst case is a stack of labelled blocks, which is
     the layout that always works. */
  display: block;
}

/* THE HEADER ROW IS MOVED OFF-SCREEN, NEVER HIDDEN. `display: none` would take it out of the
   accessibility tree and break the association between each cell and its column. Off-screen keeps
   it, the explicit ARIA roles in the markup keep the table semantics once `display` changes, and
   the visible labels come from `data-label` as generated content: decoration on top of a structure
   that is still correct underneath. */
.ct-data thead {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ct-data tbody,
.ct-data tr,
.ct-data td,
.ct-data tbody th { display: block; width: auto; }

/* A `table-caption` inside a table that is no longer a table gets an anonymous table wrapper of
   minimum width, and the caption renders as a column of single words about 66px wide. Seen on the
   real page at 320px and 390px before this rule existed. As a block it sits above the rows, which
   reads as a lead-in, so caption text is written to work either side of the data. */
.ct-data caption {
  display: block;
  margin: 0 0 16px;
  font-size: var(--ct-t-micro);
  line-height: 1.55;
  color: var(--textQuiet);
  text-align: left;
  text-wrap: pretty;
}

/* TWO TILES PER ROW IN THE STACKED STATE. One card per row gave a phone four labelled lines inside
   a full-width band and then a long scroll of the same shape: very little information in a lot of
   vertical space (Paul, 2026-08-23). Two-up halves the height of the column, and each row still
   reads as its own card because the gap between tiles is wider than the padding inside one. The
   grid replaces the old `tr + tr` margin, so a tile never carries a stray top margin in a row. */
.ct-data tbody {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}
.ct-data tbody tr {
  padding: 14px 14px 16px;
  border-radius: var(--radius-inner);
  background: var(--tint-recess);
  min-width: 0;
}
.ct-data td,
.ct-data tbody th { padding: 0; border-top: 0; text-align: left; font-weight: 400; }

/* The row's own name is its title in this state, so it takes the space a heading takes. */
.ct-data .ct-data-name { margin-bottom: 12px; color: var(--text); }
.ct-data .ct-data-name b {
  display: block;
  font-size: var(--ct-t-h3);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-bottom: 3px;
  /* A tile is about half the band wide, and the longest row names are unbroken identifiers. */
  overflow-wrap: break-word;
}
.ct-data .ct-data-name span { font-size: var(--ct-t-caption); color: var(--textQuiet); overflow-wrap: break-word; }

/* THE LABEL SITS ABOVE THE VALUE, NOT BESIDE IT, and the reason is worth keeping. Beside-it was
   built first, as a two-column grid with the label from `::before`. A cell holding a value AND a
   `.sub` line then has THREE grid items (the generated label, an anonymous item for the text node,
   and the span), so the sub line auto-placed into the next row's FIRST column and rendered under
   the label instead of under its own value. Forcing it into column two works until the narrowest
   breakpoint collapses the grid to one column, where the same rule creates an implicit second one.
   Label above value has one item per line, no ordering to get wrong, and it still fits at 280px,
   where a 96px label column left under 60px for the value and pushed the page sideways. */
.ct-data td[data-label] {
  display: block;
  padding: 8px 0;
  border-top: 1px solid var(--hairline);
  line-height: 1.45;
  color: var(--textQuiet);
  overflow-wrap: break-word;
}
.ct-data td[data-label]::before {
  content: attr(data-label);
  display: block;
  margin-bottom: 3px;
  font-size: var(--ct-t-micro);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--textQuiet);
}
.ct-data .num { font-variant-numeric: tabular-nums; color: var(--text); }
.ct-data .sub { display: block; font-size: var(--ct-t-micro); color: var(--textQuiet); }
/* A PROVENANCE SENTENCE INSIDE A NUMERIC CELL IS A SENTENCE, so it takes the cell's nowrap and
   tabular figures back off. Both are right for the figure above it and wrong for prose: left to
   run, one such note reached 1,135px inside a 178px cell and pushed the document to 1,887px at a
   1,440px window, with no element's own right edge crossing the viewport to give it away. */
.ct-data .num .sub { white-space: normal; font-variant-numeric: normal; }

/* ---- Promoted to a real table, when the band it sits in is wide enough to hold one ---- */
@container ct-band (min-width: 900px) {
  /* FIXED LAYOUT, NOT AUTO, and this is the rule that makes the module safe rather than lucky.
     With `auto`, a table is at least as wide as its content wants to be, and a six-column
     comparison of sentences wanted 1,185px inside a 1,108px band: it spilled out of its own sheet at
     every desktop width and pushed the page sideways below 1,200px. Nothing in the markup was wrong
     and no automated check caught it, because the page only scrolls once the spill clears the
     window. Fixed layout makes the table exactly as wide as its container and lets the text wrap,
     so overflow stops being possible instead of being tuned away.

     THE COST IS THAT COLUMN WIDTHS ARE NOW THE AUTHOR'S JOB. Give the table a <colgroup> with a
     percentage per column. Without one the columns are equal, which is rarely what a comparison
     wants. */
  .ct-data {
    display: table;
    table-layout: fixed;
    width: 100%;
  }
  .ct-data td,
  .ct-data th { overflow-wrap: break-word; }
  .ct-data thead {
    position: static;
    display: table-header-group;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
  }
  .ct-data tbody { display: table-row-group; }
  /* The default `.ct-data tr { display: block }` catches the HEADER row too; without this the
     thead's cells wrap in an anonymous shrink-to-fit table and bunch at the left edge. */
  .ct-data thead tr { display: table-row; }
  .ct-data tbody tr {
    display: table-row;
    padding: 0;
    margin: 0;
    border-radius: 0;
    background: none;
  }
  .ct-data tbody tr + tr { margin-top: 0; }
  .ct-data td,
  .ct-data tbody th { display: table-cell; }
  /* EVERY COLUMN IS ITS OWN COLUMN, CENTRED ON ONE AXIS (Paul, 2026-10-18: "Each of these should be
     treated as an individual column with the result centred so that none is misaligned due to
     content matchin other columns - this is a common mistake, add this to the rules for making
     tables"; applied to this module 2026-10-23, CANON W27). Header and cells are centred, and every
     cell carries the SAME inline padding on both sides, so a header's centre and its cells' centres
     are one line; the old 0 / 16px pad put each column's text 8px off its own box centre, and the
     14px outer pads moved the first and last columns again. Measured at 1440 on every page that uses
     the module: header and cell centres within 1px. The stacked tiles below 900px are untouched. */
  .ct-data td[data-label] {
    display: table-cell;
    padding: 13px 14px;
  }
  .ct-data td[data-label]::before { content: none; }
  .ct-data td,
  .ct-data tbody th {
    padding: 13px 14px;
    text-align: center;
    border-top: 1px solid var(--hairline);
    vertical-align: top;
    line-height: 1.45;
    color: var(--textQuiet);
  }
  .ct-data tbody tr {
    transition-property: background-color;
    transition-duration: var(--dur-tint);
    transition-timing-function: var(--ease-out);
  }
  .ct-data tbody tr:hover { background-color: var(--tint-recess); }
  .ct-data .ct-data-name { min-width: 150px; margin-bottom: 0; }
  .ct-data .ct-data-name b { font-size: var(--ct-t-body); margin-bottom: 2px; }
  .ct-data .ct-data-name span { font-size: var(--ct-t-micro); }
  .ct-data .num { white-space: nowrap; }
  .ct-data .nowrap { white-space: nowrap; }
  /* A figure cell too long for its column (a time with words, "218 agent-minutes; about 3 h 20 min
     on the clock") keeps the figure colour but wraps: under table-layout: fixed a nowrap cell wider
     than its column spills past it and off its header's centre (Overhaul, 103px past the table edge
     at 1440; found 2026-10-23). */
  .ct-data .num.ct-data-wrap { white-space: normal; }
  .ct-data caption {
    display: table-caption;
    caption-side: bottom;
    margin: 14px 0 0;
    /* On the prose rail like every other running-text block, matching .ct-figure figcaption and the
       ruling the two-phones .tp-data caption set (Paul, 2026-08-15): the table centres on the page
       axis, so an even inline pad to the measure lands the caption text on the same rail as the
       paragraphs. max() guards a table narrower than the measure (pad would otherwise go negative). */
    padding-inline: max(0px, calc((100% - var(--ct-measure)) / 2));
  }
}

/* Centred with the same even 14px pad as the body cells (the column rule in the container query
   above). The header row is visually hidden in the stacked state, so this changes only the table. */
.ct-data thead th {
  text-align: center;
  font-weight: 500;
  font-size: var(--ct-t-micro);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--textQuiet);
  padding: 11px 14px;
  /* Wraps: under table-layout: fixed a nowrap header wider than its column overhangs the body
     rows and the sheet edge (The Model That Heard America, 2026-09-22). Short headers are unchanged. */
  white-space: normal;
  vertical-align: bottom;
  background: var(--tint-hover);
  box-shadow: inset 0 -1px 0 var(--hairline);
}
.ct-data thead th:first-child { border-top-left-radius: 10px; }
.ct-data thead th:last-child { border-top-right-radius: 10px; }

/* PROSE COLUMNS SHARE A LEFT EDGE INSTEAD. A column whose cells are running sentences that wrap
   ("What the recording holds") reads badly centred: every line starts somewhere new. The table
   names such columns by number, `<table class="ct-data" data-prose="2 4">`, and header and cells
   of those columns sit on one left axis, the same 14px in from the column edge, so the header still
   lines up with its cells (measured within 1px). Use it only where cells wrap; short values stay
   centred under Paul's rule above. This is the orchestrator's reading of that rule's aim (no column
   misaligned against its header), recorded 2026-10-23 for Paul to overrule. Stacked tiles are
   left-aligned already, so this changes only the wide table. */
.ct-data[data-prose~="1"] :is(thead th, td, tbody th):nth-child(1),
.ct-data[data-prose~="2"] :is(thead th, td, tbody th):nth-child(2),
.ct-data[data-prose~="3"] :is(thead th, td, tbody th):nth-child(3),
.ct-data[data-prose~="4"] :is(thead th, td, tbody th):nth-child(4),
.ct-data[data-prose~="5"] :is(thead th, td, tbody th):nth-child(5),
.ct-data[data-prose~="6"] :is(thead th, td, tbody th):nth-child(6),
.ct-data[data-prose~="7"] :is(thead th, td, tbody th):nth-child(7),
.ct-data[data-prose~="8"] :is(thead th, td, tbody th):nth-child(8) { text-align: left; }

/* ---- BARS: a labelled magnitude comparison ----
   The axis is drawn and labelled rather than implied. On a log axis especially, an unlabelled
   scale is a chart that flatters its smallest bar. */
.ct-bars {
  display: grid;
  grid-template-columns: minmax(120px, max-content) minmax(0, 1fr) max-content;
  align-items: center;
  gap: 9px 16px;
  margin: 0;
  font-size: var(--ct-t-small);
}
.ct-bars dt { color: var(--textQuiet); white-space: nowrap; }
.ct-bars dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
  white-space: nowrap;
  color: var(--text);
}
/* The track is the SCALE, so it must be visible for its whole length: a bar you can see against a
   trough you cannot is a bar with no scale behind it. */
.ct-track {
  position: relative;
  height: 11px;
  border-radius: 3px;
  background: var(--tint-hover);
  overflow: hidden;
}
.ct-bar {
  /* The datum arrives as an inline `--w` on each bar. The 0 here is the defined default for a page
     that links this sheet without using the module (layers.test.js polices undefined variables):
     a bar with no datum draws nothing rather than a full-width lie. */
  --w: 0;
  position: absolute;
  inset: 0 auto 0 0;
  width: 100%;
  border-radius: 3px;
  background: rgba(16, 185, 129, 0.62);
  transform-origin: left center;
  transform: scaleX(var(--w));
}
/* ---- BARS, GROUPED: several bars belonging to one subject ----
   Rows tighten so a group reads as one block, and `.ct-bars-split` is the break before the next
   subject. The split spans every column so the grid keeps its three-column shape. */
.ct-bars--grouped { row-gap: 3px; }
.ct-bars-split { grid-column: 1 / -1; height: 12px; }

/* The second series. OBP Orange (CANON 14 accent), used against the default green so two
   regimes can sit in one chart without a legend. */
.ct-bar--onboard { background: rgba(234, 88, 12, 0.62); }

/* A RANGE rather than a value: it starts at `--from` and ends at `--to`, both defaulting to 0 so
   a bar with no datum draws nothing (layers.test.js polices undefined variables). Drawn dashed
   and faded in the chart yellow (--data-sun, #FBCB3A; Paul's instruction, 2026-08-26), so it
   cannot be read as a measurement the way a solid bar can. */
.ct-bar--gap {
  --from: 0;
  --to: 0;
  inset: 0 auto 0 calc(var(--from) * 100%);
  width: calc((var(--to) - var(--from)) * 100%);
  transform: none;
  background: repeating-linear-gradient(
    135deg,
    rgba(251, 203, 58, 0.55) 0 3px,
    rgba(251, 203, 58, 0.12) 3px 7px
  );
  box-shadow: inset 0 0 0 1px rgba(251, 203, 58, 0.45);
}

/* The PRIOR series: the same subject on an earlier regime, a day already finished and no longer
   in question. Drawn in the palette default de-emphasised series colour (--data-quiet) at the
   same 62% alpha the other solid series use, so it sits behind the two coloured regimes without
   competing with them for a reading. */
.ct-bar--prior { background: color-mix(in srgb, var(--data-quiet) 62%, transparent); }

/* The PROJECTED REMAINDER of a live bar: it starts where the measured part stopped (`--from`)
   and runs to the expected end (`--to`), both defaulting to 0 so a bar with no datum draws
   nothing (layers.test.js polices undefined variables). Same green as the solid `.ct-bar` it
   continues, so the pair reads as one series, and DOTTED rather than the 135deg dashes of
   `.ct-bar--gap`, so the two kinds of projection stay apart by pattern as well as by hue. */
.ct-bar--remain {
  --from: 0;
  --to: 0;
  inset: 0 auto 0 calc(var(--from) * 100%);
  width: calc((var(--to) - var(--from)) * 100%);
  transform: none;
  background-image: radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.8) 0 1.5px, rgba(16, 185, 129, 0) 1.9px);
  background-size: 6px 5.5px;
  box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.34);
}
.ct-axis {
  position: relative;
  grid-column: 2;
  height: 17px;
  margin-top: 3px;
  font-size: var(--ct-t-micro);
  color: var(--textQuiet);
}
.ct-axis span {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-variant-numeric: tabular-nums;
}
.ct-axis span::before {
  content: '';
  position: absolute;
  left: 50%;
  top: -7px;
  width: 1px;
  height: 5px;
  background: var(--hairline);
}
/* The first tick sits ON the origin, so centring its label hangs half of it over the label column
   and it reads as belonging to the row above. */
.ct-axis span:first-child { transform: translateX(0); }
.ct-axis span:first-child::before { left: 0; }
/* And the last tick sitting ON the far edge hangs half its label off the track, the mirror case. */
.ct-axis span.at-end { transform: translateX(-100%); white-space: nowrap; }
.ct-axis span.at-end::before { left: auto; right: 0; }

.ct-caption {
  margin: 14px auto 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-micro);
  line-height: 1.55;
  color: var(--textQuiet);
  text-wrap: pretty;
}

/* ---- HERO: the landscape banner under the masthead ---- */
/* Full page width, above the measure-width prose, and imagery only: no baked text (CANON 25). A
   caption is optional and lives OUTSIDE the frame, in `figure.ct-hero-figure` (below). It carries
   the image edge every figure image gets, in pure white at low alpha, never a tinted neutral,
   which picks up the surface under it and reads as dirt on the border. */
.ct-hero {
  margin: 6px 0 24px;
  border-radius: var(--radius-inner);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
}
.ct-hero img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: inherit;
}

/* THE HERO CAPTION (Paul, 2026-09-22: the header bird stays, "with a reason WHY it is there").
   Optional. One or two sentences saying why this bird fronts the page, which is a fact the carving
   cannot carry (CANON 25), never a description of the picture. The figure takes over the hero's
   outer spacing so the caption sits close under the image and the title keeps its usual gap.
   Only the block margins move: the phone rule below bleeds the hero sideways with inline margins,
   and those must still win. The caption sits bare on the photograph, so it takes .ct-onphoto. */
/* THE HERO PLAY CONTROL, built by ct-omni.js on a `.ct-hero` carrying `data-omni-play="<figure
   id>"` (DIRECTION.md bold move B). OPT-IN AND PREVIEW ONLY: CANON 25 keeps the carving imagery
   only, and whether a play control beside it fits that rule is Paul's to rule (DIRECTION Q3). The
   script puts the button in the page flow DIRECTLY UNDER the carving, never over it: over the lower
   left corner it covered the swift parrot's tail (species design pass, 2026-10-13), and a carving's
   subject can sit anywhere in the frame. The button is the CARD recipe (the hard scrim, then the
   white tint, then the lift shadow), because it sits on the photograph with no blur under it; its
   play square is the analyser's own green. The progress line still runs along the carving's lower
   edge from the same `audio.currentTime` the analyser's playhead reads, written into `--hero-p`. */
.ct-hero { position: relative; }
.ct-hero:has(+ .ct-hero-play) { margin-bottom: 12px; }
.ct-hero-play {
  width: fit-content;
  margin: 0 0 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100% - 36px);
  padding: 8px 16px 8px 8px;
  border: 0;
  border-radius: var(--radius-inner);
  background-color: var(--scrim-hard);
  background-image: linear-gradient(var(--tint-rest), var(--tint-rest));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.28), 0 10px 28px rgba(0, 0, 0, 0.30);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ct-hero-play-btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: var(--swiftGreenBright);
  color: var(--canopy);
  transition: background-color 150ms var(--ease-out);
}
.ct-hero-play:hover .ct-hero-play-btn { background: var(--swiftGreen); }
.ct-hero-play:focus-visible { outline: 2px solid var(--swiftGreenBright); outline-offset: 3px; }
.ct-hero-play-btn svg { width: 14px; height: 14px; display: block; }
.ct-hero-play .ct-hero-i-pause,
.ct-hero-play.is-playing .ct-hero-i-play { display: none; }
.ct-hero-play.is-playing .ct-hero-i-pause { display: block; }
.ct-hero-play-text { display: grid; gap: 2px; min-width: 0; }
.ct-hero-play-text b { font-size: var(--ct-t-body); font-weight: 600; line-height: 1.3; }
.ct-hero-play-text span { font-size: var(--ct-t-small); color: var(--textQuiet); font-variant-numeric: tabular-nums; }
.ct-hero-progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  width: calc(var(--hero-p, 0) * 100%);
  background: var(--swiftGreenBright);
  pointer-events: none;
}
@media (max-width: 640px) {
  .ct-hero-play { padding: 6px 12px 6px 6px; }
  .ct-hero-play-btn { width: 36px; height: 36px; }
}
@media (prefers-reduced-motion: reduce) {
  .ct-hero-play-btn { transition: none; }
}

.ct-hero-figure { margin: 6px 0 24px; }
.ct-hero-figure > .ct-hero { margin-top: 0; margin-bottom: 0; }
.ct-hero-figure > .ct-hero:has(+ .ct-hero-play) { margin-bottom: 12px; }
.ct-hero-figure > .ct-hero-play { margin-bottom: 0; }
.ct-hero-figure > figcaption {
  margin: 10px auto 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-caption);
  line-height: 1.55;
  color: var(--textQuiet);
  text-wrap: pretty;
}

/* ---- FIGURE: an image or an SVG with a caption ---- */
.ct-figure {
  margin: 0;
  padding: 16px;
  border-radius: var(--radius-inner);
  background: var(--tint-recess);
  display: flex;
  flex-direction: column;
}
.ct-figure > img,
.ct-figure > svg,
.ct-sono > img,
.ct-omni-plate > img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}
/* Pure white at low alpha on an image edge, never a tinted neutral: a tinted edge picks up the
   surface under it and reads as dirt along the border. */
.ct-figure > img,
.ct-sono > img,
.ct-omni-plate > img { box-shadow: inset 0 0 0 1px var(--imgEdge); outline: 1px solid var(--imgEdge); outline-offset: -1px; }
.ct-figure figcaption,
.ct-sono figcaption,
.ct-lightbox figcaption {
  /* On the rail like every other running-text block: a caption spanning a wide figure frame
     directly above prose at the measure was two widths stacked in one surface. */
  margin: 14px auto 0;
  max-width: var(--ct-measure);
  width: 100%;
  font-size: var(--ct-t-caption);
  line-height: 1.55;
  color: var(--textQuiet);
  text-wrap: pretty;
}
/* A pair of figures is given ONE height and each centred in it. They are different shapes, and
   letting each take its natural height puts the captions on two different lines. */
.ct-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 20px;
  align-items: start;
}

/* ---- SONOGRAM WITH AUDIO (`ct-sono`): the plate, the recording it was drawn from, and the
       caption, in ONE surface. A sonogram never ships without its playable audio, so this module
       is the only way a plate goes on a page (Paul, 2026-08-31).

       IT IS DELIBERATELY NOT A `ct-figure`. A figure carries the recess tint, and a recess inside
       the well or sheet that holds it is a box drawn inside a box for no reason (Paul, 2026-08-31:
       "there should not be two boxes that exist for no reason ever"). The surface is whatever the
       page already put the module in; this draws the plate, the player and the caption on it. */
.ct-sono { margin: 0; }
.ct-sono + .ct-sono { margin-top: 26px; }
/* A plate's subheader: the day, the time and the weather, above the plate rather than buried in
   the caption (Paul, 2026-09-07). Plain text, the caption tone, no rule under it. */
/* It sits on the measure, level with the prose above and the caption below (the caption is capped
   at --ct-measure too), so in a wide band the plate breaks out and the words keep one left edge;
   at the plate's edge it stood 150px left of the credit under the same plate (2026-10-13). */
.ct-sono-head {
  max-width: var(--ct-measure);
  margin: 0 auto 10px;
  font-family: var(--fontUI);
  font-size: var(--ct-t-small);
  font-weight: 600;
  line-height: 1.4;
  color: var(--textQuiet);
}

/* THE PLAYER RAIL IS THE PLATE'S OWN TIME AXIS. `tools/sonogram/sonogram.py` places the plot with
   fig.add_axes([0.075, 0.13, 0.9, 0.80]), so the drawn seconds start 7.5% in from the image's left
   edge and end 2.5% short of its right. The rail is inset by exactly those two numbers, which is
   what makes the playhead sit under the second it is playing. A plate framed differently restates
   them on `.ct-sono-player` itself, never the figure root: this rule sets them on the player, so a
   value on the root never reaches the rail (`.ct-ears .ct-sono-player` and the species reference
   plate both do it this way; found 2026-10-23). */
.ct-sono-player {
  --sono-l: 7.5%;
  --sono-r: 2.5%;
  margin-top: 8px;
}
.ct-sono-track {
  position: relative;
  height: 14px;
  cursor: pointer;
  touch-action: manipulation;
}
/* Same white outline as .ct-sono-play:focus-visible above, reused rather than invented: both
   controls sit on the same dark glass strip where the global green ring would not read. */
.ct-sono-track:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 2px; }
.ct-sono-rail {
  position: absolute;
  left: var(--sono-l);
  right: var(--sono-r);
  top: 50%;
  height: 4px;
  margin-top: -2px;
  border-radius: 2px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.14);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10);
}
.ct-sono-fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 2px;
  background: rgba(250, 250, 245, 0.85);
}
/* The controls take the same inset as the rail, so the button, the rail and the plot area of the
   plate all start on one vertical line. */
.ct-sono-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
  padding-left: var(--sono-l);
  padding-right: var(--sono-r);
}
/* The glass step, transparent and small: a control, never a fifth material. */
.ct-sono-play {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--tint-rest);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: var(--text);
  cursor: pointer;
  transition: background 140ms ease, box-shadow 140ms ease;
}
.ct-sono-play:hover { background: rgba(255, 255, 255, 0.12); }
.ct-sono-play:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 2px; }
/* The two icons swap on one class. Both hide rules carry the button class so they outrank
   `.ct-sono-play svg`, which is one element more specific than a bare icon class and drew both
   icons at once until it did. */
.ct-sono-play svg { display: block; }
.ct-sono-play .ct-sono-i-pause { display: none; }
.ct-sono.is-playing .ct-sono-play .ct-sono-i-play { display: none; }
.ct-sono.is-playing .ct-sono-play .ct-sono-i-pause { display: block; }
/* Tabular figures, so the readout does not shuffle its own width as it counts. */
.ct-sono-time {
  margin-left: auto;
  font-size: var(--ct-t-small);
  font-variant-numeric: tabular-nums;
  color: var(--textQuiet);
}
.ct-sono-nojs { margin: 12px 0 0; font-size: var(--ct-t-small); }
@media (prefers-reduced-motion: reduce) {
  .ct-sono-play { transition: none; }
}

/* ---- OMNICHEEP ANALYSER (`ct-omni`): one recording read three ways on ONE surface. The plate
       above, the transport in the middle, a live spectrum under it, so the second you are hearing
       is marked on the picture and the frequencies in it are drawn at the same moment.

       IT EXTENDS `ct-sono`, IT DOES NOT REPLACE IT. The figure carries both classes, every rule
       above still applies to it, and the sonogram player script above still drives the transport.
       Nothing here restates a ct-sono declaration. What it adds: a positioned wrapper for the
       plate, because the playhead needs a parent to sit in; the playhead; and the spectrum.

       NO SECOND BOX. Like ct-sono, this draws on whatever surface the page already put it on
       (Paul, 2026-08-31: "there should not be two boxes that exist for no reason ever"). The
       canvas is a bare element with no fill and no frame of its own.

       THE GEOMETRY IS THE PLATE'S OWN. `tools/sonogram/sonogram.py` places the plot with
       fig.add_axes([0.075, 0.16, 0.9, 0.77]): 7.5% in from the left, 90% wide, and in image
       coordinates 7% down from the top to 16% up from the bottom. Those four numbers put the
       playhead over the second it is playing and stop it crossing the axis labels; the rail and
       the spectrum take the same inset, so the plot area, the transport and the spectrum all
       start and end on one vertical line. A plate framed differently restates them on the root. */
.ct-omni {
  --omni-l: 7.5%;
  --omni-r: 2.5%;
  --omni-w: 90%;
  --omni-t: 7%;
  --omni-b: 16%;
  /* Playback position, 0 to 1, written by the script. The geometry stays here in the cascade so
     there is one home for it rather than the same percentages in two languages. */
  --omni-p: 0;
  /* HOW FAR THE TRANSPORT BODY REACHES PAST THE PLATE, the same amount at both ends. Each control
     sits ON the body with equal air either side of it (Paul, 2026-09-15: "the pill is NOT ON THE
     BACKGROUND, it should be equidistant to the left of the pill and the start of the scrubber
     bar"), so each inset has to hold its control plus that air: 22px on the right for the 16px
     glyph, 36px on the left for a pill no thinner than 28. The insets are percentages of the
     plate, so on a narrow band they cannot, and the body reaches past the plate to make the room.
     It reaches by the SAME amount at both ends, because a body that overhangs on one side only
     reads as a pill that slipped. Zero wherever both insets are already wide enough. Negative, so
     it can be used as an offset directly. */
  --omni-over: min(0px, calc(var(--omni-r) - 22px), calc(var(--omni-l) - 36px));
  /* The pill is the concept sheet's 38px wherever the left inset can hold it with 4px of air
     either side, and takes what is there below that, never under 28px because `--omni-over`
     bought the room. */
  --omni-pill: min(38px, calc(var(--omni-l) - var(--omni-over) - 8px));
}
.ct-omni-plate { position: relative; }
.ct-omni-head {
  position: absolute;
  top: var(--omni-t);
  bottom: var(--omni-b);
  left: calc(var(--omni-l) + var(--omni-p) * var(--omni-w));
  width: 1px;
  /* Near-white, and it stays near-white while the rail fill below it turns green. The rail sits
     on the page's own dark glass, where green reads as the loudest thing on the surface; this
     line lies on a dark green plate, where the same green would disappear into it. The scrim ring
     keeps it readable where the plate is bright. */
  background: rgba(250, 250, 245, 0.85);
  box-shadow: 0 0 0 1px var(--scrim-soft);
  pointer-events: none;
  /* NO TRANSITION. The script writes `--omni-p` on every animation frame, so the line is already
     moving at the frame rate; a 90ms ease on top of that made it chase the position it had just
     been given, and the rail's thumb, written in the same frame with no transition, then ran ahead
     of it (Paul, 2026-09-15: "it lags beind a dozen pixels than jumps ahead then lags, it should
     move smoothly perfectly in line with the sonograph"). Both marks are now written from one
     function off one `audio.currentTime` read. */
}
/* THE SPECTRUM IS THE PLATE'S TWIN, the same width as the plot area and the same height as the
   picture above it, so the module reads as two equal pictures with the transport between them
   (Paul, 2026-09-14: "the height of the actual bottom (Spectrum) is WAY shorter than the
   sonograph"). The arithmetic: the plate is a 1000x300 image drawn at the figure's full width, so
   it renders 0.30 of that width tall, and this box is 90% of the same width. 0.90 over 0.30 is
   3 to 1. Written as a ratio rather than a height, it tracks the band it sits in at every width
   and needs no breakpoint. */
.ct-omni-spectrum {
  margin: 10px var(--omni-r) 0 var(--omni-l);
  aspect-ratio: 3 / 1;
}
.ct-omni-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* THE TRANSPORT CARRIES THIS MODULE, so it is loud here and quiet everywhere else. Under a
   `ct-sono` plate the player sits beside a paragraph as evidence and a 26px glass step is right.
   Here the recording IS the subject and the control sits between two full pictures, where that
   step vanished (Paul, 2026-09-14: "the play button is almost invisible", "use the twote green
   for the button"). Every rule below is scoped to `.ct-omni`, so a plate on any other page keeps
   the quiet control exactly as it was. The button holds its fill whether the clip is playing or
   paused: a control that empties out when you stop it is the faint ring again. It is a thin
   rounded pill rather than a circle, 38 by 16 at reading width, because that is the shape on
   Paul's own layout sheet at `docs/design/concepts/simple audio.tsv`, where `cmp_play` is 38 by
   14 on a 402 by 18 canvas. It stays the anchor of the row: the only solid green object on the
   pill, and the one the reader's eye lands on first. Its glyph is the play triangle while the
   clip is paused and the pause bars while it runs, swapped by `.ct-sono.is-playing` in the base
   rules above. */
.ct-omni .ct-sono-play {
  position: absolute;
  top: 6px;
  bottom: 6px;
  /* CENTRED IN THE ROOM BETWEEN THE BODY'S LEFT EDGE AND THE BAR'S START, so the air to its left
     equals the air to its right and the pill reads as sitting ON the body rather than hung off
     its end. The room is `--omni-l - --omni-over`; the pill takes `--omni-pill` of it and the
     remainder is halved. */
  left: calc(var(--omni-over) + (var(--omni-l) - var(--omni-over) - var(--omni-pill)) / 2);
  width: var(--omni-pill);
  height: auto;
  border-radius: 8px;
  background: var(--swiftGreenBright);
  box-shadow: none;
  color: var(--canopy);
}
.ct-omni .ct-sono-play:hover { background: var(--swiftGreen); }
.ct-omni .ct-sono-play svg { width: 10px; height: 10px; }
/* The visible control is 16px tall because the row is 16px tall. The thing a thumb has to hit is
   40px, so an invisible box is hung off the button rather than the button being drawn bigger.
   Asymmetric for the same reason the loop glyph's is: the bar's own reach begins at the bar's
   left end, so this one stops 4px short of it and takes the room it needs on the outer side,
   where the only thing beside it is the body's rounded end. */
.ct-omni .ct-sono-play::before {
  content: "";
  position: absolute;
  inset: -12px -2px -12px -8px;
}

/* NO TIMESTAMP. Paul, 2026-09-15: "There is no timestamp." The readout element stays in the
   markup and stays current, because the sonogram player's script writes to it and a screen
   reader still reads the seek position off the slider's `aria-valuetext`. It is only unpainted. */
.ct-omni .ct-sono-time { display: none; }

/* THE LOOP CONTROL IS A GLYPH AND NOTHING ELSE (Paul, 2026-09-15: "loop toggle", "There is no
   timestamp", "just please make a simple fucking audio player interface"). At rest it is the
   light glyph on the pill with no ring, no fill and no box: the action on this row is play, and
   looping is an option sitting quietly beside it. Pressed, it takes the play button's green fill
   and the same dark glyph, so the state is read off the two materials already on the row. The
   word `Loop` is gone; two chasing arrows say repeat, and the name reaches a screen reader
   through `aria-label` and `aria-pressed` and a pointer through `title`. */
.ct-omni-loop-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  box-shadow: none;
  cursor: pointer;
}
.ct-omni-loop-btn svg { width: 12px; height: 12px; }
.ct-omni-loop-btn:hover { color: var(--swiftGreenBright); }
.ct-omni-loop-btn[aria-pressed="true"] {
  background: var(--swiftGreenBright);
  color: var(--canopy);
}
.ct-omni-loop-btn[aria-pressed="true"]:hover { background: var(--swiftGreen); color: var(--canopy); }
/* The same invisible 40px reach the play button carries, for the same reason. It is asymmetric
   because the bar's own reach ends at the bar's right end and the glyph starts 5px past it: a
   symmetric 6px reach here would overlap the bar's by a pixel, so the left side takes 2px and the
   room the reach needs is taken on the right, where there is nothing to collide with. */
.ct-omni-loop-btn::before {
  content: "";
  position: absolute;
  inset: -12px -6px -12px -2px;
}

/* THE TRANSPORT IS ONE OBJECT (Paul, 2026-09-15: "The player itself needs to have some kind of
   pill around it, from the play button to the loop button, incorporating the progress bar, it
   should stand out a little from the drab background"). The button, the rail, the readout and the
   loop box sit on ONE raised surface instead of on two loose lines. Nothing in the markup moved:
   the row is assembled by handing the controls' children to the strip and ordering them across
   it, so the sonogram player's script still finds every element exactly where it left it.

   THE SPEC IS PAUL'S LAYOUT SHEET, `docs/design/concepts/simple audio.tsv`, and it is short
   (Paul, 2026-09-15): "scrubber bar, thin play button, loop toggle. Play button toggles to pause
   when playing and play when paused. That is is. I want this in the twote colours. Start and end
   of the bar matches the start and end of the two graphs above/below. The play button is in FRONT
   of that bar. The loop button is at the back. There is no timestamp. Keep the colours of the
   button and bar as they are, same with the background. Ensure that there is no collision with
   the elements, just please make a simple fucking audio player interface." The sheet draws one
   402 by 18 canvas holding three objects: `cmp_play` at x 0, 38 by 14; `cmp_scrubber` at x 46,
   325 by 12, with a track, a 3 by 13 thumb and a progress fill; `cmp_loop` at x 376, 26 by 18.

   THE DESIGN DECISIONS, written down because the next reader will otherwise undo them.

   1. ONE ROW, READ LEFT TO RIGHT: play pill, bar, loop glyph, nothing else and nothing stacked.
      "In front" and "at the back" are reading positions on that row, which is how the sheet
      draws them, and none of the three touches another.
   2. THE BAR'S ENDS ARE THE PLATE'S PLOT EDGES, 7.5% in from the plate's left and 2.5% short of
      its right, the same numbers the playhead above and the spectrum below take (Paul,
      2026-09-15: the twelve second scrubber must match the start and end points of the graphs,
      "that is a KEY compoenent of the functionality"). The pill carries no horizontal padding, so
      its padding box equals the plate; the three objects are absolutely positioned inside it and
      the bar's two insets are those same percentages, which track the plate at every width with
      no breakpoint. Absolute positioning rather than a flex row is what keeps this exact: a bar
      that is a flex item between two controls measures the space left over, never the plot.
   3. EACH CONTROL SITS ON THE BODY WITH EQUAL AIR EITHER SIDE OF IT, in the inset beside the bar,
      which is where the sheet puts them and what keeps them off it (Paul, 2026-09-15: "the pill is
      NOT ON THE BACKGROUND, it should be equidistant to the left of the pill and the start of the
      scrubber bar"). The play pill is centred in the room between the body's left edge and the
      bar's start, the loop glyph in the room between the bar's end and the body's right edge.
      Where an inset is too thin to hold its control with that air, the body reaches past the
      plate by `--omni-over`, the same amount at both ends. The bar never moves for either of
      them: measured on the preview, the air runs 4.0 and 4.0 either side of the pill at 390 and
      17.3 and 17.3 at 1200, and the bar's ends sit on the plot edges to 0.01px.
   4. THE RAIL IS A GROOVE WITH A THUMB IN IT. Eight pixels deep rather than the four pixel
      hairline it was, so it reads as a track something runs along; the fill is the same green as
      the button and the spectrum's trace, all three meaning the second being heard. The trough is
      cut INTO the pill, darker than the surface around it with a shadow along its top lip, so the
      empty part of the rail reads as depth instead of as a second pale bar. At the fill's leading
      edge stands the thumb, 3px wide and a little taller than the groove, in the same near-white
      as the playhead on the plate: one instant marked in two places in one colour. The sheet
      draws the same object, 3 by 13 against a 12 tall track.
   5. THERE IS NO READOUT. The time element is still written by the script and still reaches a
      screen reader through the slider's `aria-valuetext`; it is simply not painted.
   6. THE VERTICAL RHYTHM IS ONE LINE: 6px of padding, a 16px row, 6px of padding, 28px of pill
      against the 69px this took when the rail and the controls sat on two lines. The controls are
      16px tall because the row is, and each carries an invisible 40px-tall reach so a thumb still
      lands on it.

   THE MATERIAL IS `.ct-panel`, THE TEMPLATE'S LOUDER INNER STEP (CANON 14, DESIGN.md materials):
   the same `--tint-rest` fill and nothing else, raised one documented step to `--tint-strong`
   because a control sitting between two full-bleed pictures has to read LIGHTER than the sheet it
   is on rather than as a darker smudge on it. It is painted by a pseudo-element rather than by
   the box's own background, because the body has to reach past the plate's right edge for the
   loop glyph while the bar's percentage insets keep resolving against the plate's own width. Only
   the radius departs from `.ct-panel`, and it departs concentrically: 14px on a 28px body keeps
   the ends round against the 8px radius of the 16px play pill. No new colour, no blur, no fifth
   material. */
.ct-omni .ct-sono-player {
  position: relative;
  display: block;
  /* Border-box, so this is 6px of padding, a 16px row and 6px of padding. The three controls are
     positioned against the padding box with `top: 6px; bottom: 6px`, which lands them on the row. */
  height: 28px;
  margin: 10px 0 0;
  padding: 6px 0;
  background: none;
}
.ct-omni .ct-sono-player::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  /* Flush with the plate wherever the right inset can hold the glyph, and past it by the same
     amount at BOTH ends where it cannot, so the glyph always has 2px of body behind it and the
     body stays symmetrical about the picture it sits under. */
  left: var(--omni-over);
  right: var(--omni-over);
  border-radius: 14px;
  background: var(--tint-strong);
}
.ct-omni .ct-sono-controls { display: contents; }
.ct-omni .ct-sono-track {
  position: absolute;
  top: 6px;
  bottom: 6px;
  left: var(--omni-l);
  right: var(--omni-r);
  height: auto;
}
/* The bar is 16px of row; the reach for a thumb is 40px, and it extends vertically only so it
   never grows over the two controls sitting either side of it. */
.ct-omni .ct-sono-track::before {
  content: "";
  position: absolute;
  inset: -12px 0;
}
.ct-omni .ct-sono-rail {
  left: 0;
  right: 0;
  height: 8px;
  margin-top: -4px;
  border-radius: 4px;
  overflow: visible;
  background: rgba(0, 0, 0, 0.30);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5), inset 0 -1px 0 rgba(255, 255, 255, 0.08);
}
.ct-omni .ct-sono-fill {
  position: relative;
  border-radius: 4px 0 0 4px;
  background: var(--swiftGreenBright);
}
.ct-omni .ct-sono-fill::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -1.5px;
  width: 3px;
  height: 14px;
  margin-top: -7px;
  border-radius: 1.5px;
  background: rgba(250, 250, 245, 0.92);
  box-shadow: 0 0 0 1px var(--scrim-soft);
}
.ct-omni .ct-omni-loop-btn {
  position: absolute;
  top: 6px;
  bottom: 6px;
  /* Centred in the room between the bar's end and the body's right edge, the pill's rule mirrored:
     the room is `--omni-r - --omni-over`, the 16px glyph takes its share and the rest is halved,
     so the air either side of the glyph matches. */
  left: calc(100% - var(--omni-r) + (var(--omni-r) - var(--omni-over) - 16px) / 2);
  height: auto;
}

/* THE IDENTITY BLOCK, the row that says what this recording is. It leads the module because a
   reader needs a handle on each one before anything else (Paul, 2026-09-15: "number the listed
   detections so that I can more easily give feedback"), which is why the number sits in the
   identity rather than in a caption under the picture. The date and time take the format Paul
   wrote, `Sep 13, 09:28`, and the zone is stated once on the page rather than on every stamp. */
.ct-omni-id { margin: 0 0 12px; }
.ct-omni-title {
  margin: 0 0 6px;
  font-size: var(--ct-t-small);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--textQuiet);
}
.ct-omni-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  margin: 0;
  font-size: var(--ct-t-body);
  line-height: 1.4;
}
.ct-omni-species { font-weight: 600; color: var(--text); }
.ct-omni-num {
  font-weight: 600;
  color: var(--textQuiet);
  font-variant-numeric: tabular-nums;
}
.ct-omni-when {
  color: var(--textQuiet);
  font-variant-numeric: tabular-nums;
}
/* The stamp goes to the far end where the row has room for it, and stays in the flow where it
   does not. Measured on the band, like every other reshape in this file. */
@container ct-band (min-width: 420px) {
  .ct-omni-when { margin-left: auto; }
}

/* THE VERDICT, the module's own reading of the recording and the reason for it. It stays INSIDE
   the analyser because it is the module's output, and a caption under the figure would read as a
   note about the picture instead (Paul, 2026-09-15).

   IT SITS UNDER THE TRANSPORT AND ABOVE THE SPECTRUM, LABELLED "Acoustician says:" (Paul,
   2026-10-13, docs/plans/2026-10-13-species-standard/ASK.md: "clearly label each confidence check
   as 'Acoustician says:'"). As the last row it landed about 650px below the detector's score pill
   at 1280, so the level and its reason never read together with the recording they judge. Here the
   score and the level are one plate apart, the reason sits beside its level, and the judgement is
   still below the recording, where Paul's own intro on the swift parrot page says it is ("You can
   see it's judgement below each recording"). One block, `.ct-omni-says`: the label, the level
   pill on the same line, then the reason at the measure. No box, no tint, no accent edge (CANON
   W1): the label and the pill carry it.

   A CONTEXT SEGMENT (the seconds either side of a call, with no call of its own) never wears its
   call's level (acoustician audit finding 3, 2026-10-13). Its block, `.ct-omni-says--context`, holds
   the label, the segment's own finding and one quiet line, `p.ct-omni-call`, naming the call's
   level and linking to the analyser that carries it ("The call: Possible, on #8").
   `tools/sync-omni-badges.mjs` keeps that line's level in step with the ledger. */
/* A GRID, NOT A WRAPPING FLEX ROW (design pass, 2026-10-13). The label and the pill share the first
   row; the reason and the call line each take a whole row under them. As a flex row the reason's
   `flex-basis: 100%` was clamped by the verdict's measure cap to 660px, which fits beside the label
   and the pill in any band wider than about 850, so at 1280 the reason ran up beside them. */
.ct-omni-says {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  justify-items: start;
  align-items: center;
  gap: 6px 10px;
  margin: 16px 0 0;
}
.ct-omni-says > .ct-omni-verdict,
.ct-omni-says > .ct-omni-call { grid-column: 1 / -1; }
.ct-omni-says-label {
  margin: 0;
  font-size: var(--ct-t-small);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--textQuiet);
}
.ct-omni-says > .ct-omni-badge-line { margin: 0; }
.ct-omni-says > .ct-omni-verdict { flex: 1 0 100%; margin: 2px 0 0; }
.ct-omni-call {
  flex: 1 0 100%;
  margin: 0;
  font-size: var(--ct-t-small);
  line-height: 1.5;
  color: var(--textQuiet);
}
.ct-omni-says + .ct-omni-spectrum { margin-top: 18px; }

/* THE COMPACT ANALYSER, `ct-omni--compact` on the figure: no spectrum. Only for a context segment
   the acoustician finds empty of the call (DIRECTION.md Q1, Paul's ruling pending); a recording
   that carries a call keeps the spectrum as the plate's twin (Paul, 2026-09-14). The canvas stays
   in the markup, unpainted, because the script wires the playhead wherever it finds one. */
.ct-omni--compact .ct-omni-spectrum { display: none; }
/* NOT BEFORE THE FIRST PLAY. Until a reader presses play the spectrum has nothing to draw but its
   axes, the reference band and a flat line along the floor, and an empty chart reads as a broken
   one (species design pass, 2026-10-13). ct-omni.js puts `is-unplayed` on each figure it wires and
   takes it off on the first `play`; from then on the box stays, holding the last frame. A class the
   script sets rather than a "not played" selector, so the clip review's own `.ct-omni`, which
   clip-review.js paints itself, keeps its spectrum. */
.ct-omni.is-unplayed .ct-omni-spectrum { display: none; }

/* THE MEASURED-NOTE MARKS, built by ct-omni.js from `data-marks` on the figure (DIRECTION.md bold
   move C). A strip directly under the plate on the plate's own time axis: a mark starts at the
   playhead's arithmetic, `--omni-l + t0 * --omni-w`, and runs `tw` of the plot, so it stands under
   the notes it names. Sun, the data ramp's second series colour (CANON 30), so it never reads as
   the transport's green or the playhead's white. The strip is 22px tall for the pointer; the bar
   drawn in it is 4px. A mark lights while the playhead is inside it. */
.ct-omni-marks { position: relative; height: 22px; margin: 4px 0 -6px; }
.ct-omni-mark {
  position: absolute;
  top: 0;
  height: 22px;
  left: calc(var(--omni-l) + var(--t0, 0) * var(--omni-w));
  width: max(12px, calc(var(--tw, 0) * var(--omni-w)));
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.ct-omni-mark--point { width: 12px; translate: -6px 0; }
.ct-omni-mark::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 9px;
  height: 4px;
  border-radius: 2px;
  background: var(--data-sun);
  opacity: 0.8;
  transition: opacity 150ms var(--ease-out), scale 150ms var(--ease-out);
}
.ct-omni-mark:hover::before,
.ct-omni-mark:focus-visible::before,
.ct-omni-mark.is-on::before { opacity: 1; scale: 1 1.75; }
.ct-omni-mark:focus-visible { outline: 2px solid var(--data-sun); outline-offset: 1px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) {
  .ct-omni-mark::before { transition: none; }
}

/* THE PAIR ROW, `.ct-omni-pair`: two compact analysers side by side, each still on its own sheet
   (Paul, 2026-09-15: one analyser per surface). Two columns wherever each gets 400px, one under the
   other below that, so it decides on the room it has rather than the window. The concept first
   tried a container query and the grid never fired, because no ancestor was a size container; an
   auto-fit grid needs no container at all. Plates in a pair are drawn for the pair's column, never
   scaled from the 1000px plate: see the ct-omni manifest entry for the recipe. */
.ct-omni-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr));
  gap: 16px;
  align-items: start;
}
.ct-section > * + .ct-omni-pair { margin-top: 16px; }
.ct-omni-pair > .ct-sheet { margin: 0; }
/* A single analyser sits in a .ct-band, whose padding sets it 20px in from the sheet's; a pair's
   analysers sit straight on their sheets and ran 16px tighter (2026-10-13). The band's own padding,
   restated here, and its phone value in the 640 block. */
.ct-omni-pair > .ct-sheet > .ct-omni { padding: 24px 20px; }

/* THE VERDICT BADGE. (This comment lost its opener on 2026-10-13 when the pair-row rules above were
   inserted, and every rule after this point stopped applying, the 640px phone layout included.)

   THE BADGE IS A CONFIDENCE PILL AND TAKES THE CANON 15 RAMP (Paul, 2026-09-15: the judgement
   pills "have not used the sliding scale of confidence that the % pills use that I SPECIFICALLY
   asked for"). It carries `.ct-pill`, so its size, padding, radius and weight are the score
   pill's in the header above to the pixel, and its colour comes from `/ramp.js`: the module's own
   script reads `data-verdict` and hands the ramp the position that step sits at. The five steps
   are five evenly spaced points on the one scale, running the ramp's whole range:

     confident      1.000   #4DFFBB
     likely         0.875   #6CF5A3
     possible       0.750   #AFDF67
     unlikely       0.625   #E5C03C
     confident_not  ----    --swiftCrimson, the one documented step off the ramp

   THE RED END IS THE ONE DEPARTURE. Steps one to four are the ramp exactly as CANON 15 draws it.
   The fifth step means the analyser is sure the species is ABSENT, which is the opposite end of
   the judgement rather than a lower point on it, and the ramp's own far end is the amber #FDA258
   that reads as a weak yes. It takes `--swiftCrimson`, the token the rest of the product already
   spends on a critical state, painted as a solid fill with `--text` on it: crimson as translucent
   TEXT on this ground measures 3.0 to 1 and fails, and the same crimson as a fill under near-white
   measures 5.2 to 1 and passes. Nothing else on the page may leave the ramp.

   Apart from that one step no colour is written here and the module owns no hue of its own: the
   `--omni-v-likely` lime that filled the second step is gone, and so are the five solid token
   fills that stood in for a scale. What is left below is the resting state, which is what the
   badge looks like before the script paints it and for a reader with no JavaScript at all.

   THE TWO CONFIDENTS ARE TOLD APART BY COLOUR AND BY A `.ct-sr` SUFFIX, never by visible words.
   Paul set the five words and they are used as he wrote them. A `confident_not` rationale opens
   by saying the direction in plain words, so the reading never rests on the colour of the badge.

   THE BADGE TAKES ITS OWN LINE. Run into the rationale it read as the paragraph's first word and
   the module's output looked like a sentence that happened to start in colour. Above it, with the
   reasoning in the body colour underneath, the verdict is the heading of its own small block. */
.ct-omni-badge-line { margin: 14px 0 0; }
.ct-omni-verdict {
  margin: 8px 0 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-body);
  line-height: 1.6;
  color: var(--text);
}
.ct-omni-badge {
  background: var(--tint-rest);
  color: var(--textQuiet);
}

/* ---- TWO EARS (`ct-ears`): two phones' recordings of one moment, lined up by their sound and
       played as one stereo pair, the first phone in the left ear and the second in the right.
       Each ear is a lane: a level strip over a spectrum plate, the scores that phone gave under
       it, and in the gutter beside it the phone, its model and a meter that moves with the level.
       The two phones that keep no audio get score-only lanes under the pair, so a reader sees
       what every phone said about the same seconds.

       IT EXTENDS `ct-sono`, IT DOES NOT REPLACE IT. The figure carries both classes and the
       sonogram player script still runs the transport: play, pause, seeking, the keyboard and
       the one-clip-at-a-time rule are all that script's. This block restates none of its rules.
       What it adds: the lanes, the playhead in each plot, the score chips, and the mix toggle
       (Left / Both / Right), which the module's own script builds into the controls row.

       THE LANES ARE DRAWN FROM THE SIDECAR ALONE. `data-ears` on the figure names the JSON
       `tools/two-ears/two_ears.py` writes; the plates, the level envelope, the chips, the offset
       and the noise floor all come from it, so a new pair is a new sidecar and no new code.

       ONE SHARED TIME AXIS, LANES STACKED AT EVERY WIDTH. Side by side, each ear would need its
       own playhead and the offset between the phones would be a thing to count rather than see.
       Stacked, the same second sits in the same column of every lane. What moves with the band
       is the gutter: beside the plate at reading width, above it in a narrow band.

       NO SECOND BOX. Like ct-sono it draws on whatever surface the page already put it on; no
       lane has a fill or a frame of its own. The two ear colours are the data palette's first
       two series (CANON 30/33), and a phone with no audio takes the palette's quiet series. */
.ct-ears {
  --ears-gut: 124px;
  --ears-tick: 34px;
  --ears-x: calc(var(--ears-gut) + var(--ears-tick));
  --ears-p: 0;
  --ears-row: 26px;
  --ear: var(--data-quiet);
}
.ct-ears-status {
  margin: 0;
  font-size: var(--ct-t-small);
  line-height: 1.5;
  color: var(--textQuiet);
}
.ct-ears[data-state="failed"] .ct-ears-status { color: var(--text); }
.ct-ears-lanes { display: grid; gap: 14px; }
.ct-ears-lane {
  display: grid;
  grid-template-columns: var(--ears-gut) var(--ears-tick) minmax(0, 1fr);
  grid-template-areas: "gut tick plot";
  align-items: start;
  transition: opacity var(--dur-tint, 150ms) ease;
}
.ct-ears-lane[data-side="left"] { --ear: var(--data-leaf); }
.ct-ears-lane[data-side="right"] { --ear: var(--data-sun); }
/* Soloing one ear dims the other lane, so what you see is what you hear. */
.ct-ears[data-mix="left"] .ct-ears-lane[data-side="right"],
.ct-ears[data-mix="right"] .ct-ears-lane[data-side="left"] { opacity: 0.35; }

.ct-ears-gutter {
  grid-area: gut;
  padding-right: 12px;
  font-size: var(--ct-t-small);
  line-height: 1.35;
  min-width: 0;
}
.ct-ears-side { display: block; font-weight: 600; color: var(--ear); }
.ct-ears-dev { display: block; color: var(--text); }
.ct-ears-model { display: block; font-size: var(--ct-t-micro); color: var(--textQuiet); }
.ct-ears-meter {
  display: block;
  margin-top: 8px;
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.14);
}
.ct-ears-meter > span {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 2px;
  background: var(--ear);
  transform-origin: left center;
  transform: scaleX(var(--lvl, 0));
}

/* The frequency labels sit in their own narrow column against the plate's left edge, at the
   heights the sidecar gives as fractions from the bottom. The lowest is the noise floor. */
.ct-ears-ticks { grid-area: tick; position: relative; align-self: stretch; }
.ct-ears-tick {
  position: absolute;
  right: 6px;
  transform: translateY(50%);
  font-size: var(--ct-t-micro);
  line-height: 1;
  color: var(--textQuiet);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.ct-ears-plot { grid-area: plot; position: relative; min-width: 0; }
.ct-ears-level { display: block; width: 100%; height: 22px; }
.ct-ears-level path { fill: var(--ear); opacity: 0.55; }
.ct-ears-plate { margin-top: 4px; }
.ct-ears-plate > img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
  outline: 1px solid var(--imgEdge);
  outline-offset: -1px;
}
/* A silent lane has no level strip or plate, so its tick column holds nothing and its scores
   sit level with its label. */
.ct-ears-lane.is-silent .ct-ears-scores { margin-top: 2px; }

/* THE PLAYHEAD, one line per plot, every one written from the same `--ears-p`, so the lanes
   read as one line through the stack. Near-white with the scrim ring, as ct-omni's is, and no
   transition: the script moves it every frame. */
.ct-ears-head {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--ears-p) * 100%);
  width: 1px;
  background: rgba(250, 250, 245, 0.85);
  box-shadow: 0 0 0 1px var(--scrim-soft, rgba(0, 0, 0, 0.35));
  pointer-events: none;
}

/* THE SCORES. One group per scored twelve second segment, a bar the segment's exact length on
   the shared axis with that phone's scores of 30% or more as pills under it; anything lower is
   counted in words. A group whose pills run wider than its bar keeps the bar true and lets the
   pills run on; the script drops a group to the next row where it would collide with one
   before it, and pulls a group back from the plot's right edge. */
.ct-ears-scores { position: relative; margin-top: 6px; min-height: var(--ears-row); }
.ct-ears-win {
  position: absolute;
  top: calc(var(--row, 0) * var(--ears-row));
  height: var(--ears-row);
  opacity: 0.6;
  transition: opacity var(--dur-tint, 150ms) ease;
}
.ct-ears-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 2px;
  height: 2px;
  border-radius: 1px;
  background: var(--ear);
}
.ct-ears-pills {
  position: absolute;
  top: 5px;
  left: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}
.ct-ears-pills .ct-pill { font-size: var(--ct-t-micro); }
.ct-ears-more { font-size: var(--ct-t-micro); color: var(--textQuiet); }
.ct-ears-win.is-live { opacity: 1; }

/* THE OFFSET AND THE FLOOR, stated plainly under the lanes: what was moved and what was cut. */
.ct-ears-facts {
  margin: 12px 0 0 var(--ears-x);
  font-size: var(--ct-t-small);
  line-height: 1.5;
  color: var(--textQuiet);
}

/* The transport starts where the plots do, so the rail is the lanes' own time axis. */
.ct-ears .ct-sono-player { --sono-l: 0%; --sono-r: 0%; margin-left: var(--ears-x); }
.ct-ears-mix { display: inline-flex; gap: 4px; }
.ct-ears-mix button {
  height: 26px;
  padding: 0 10px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: var(--textQuiet);
  font: inherit;
  font-size: var(--ct-t-small);
  cursor: pointer;
  transition: background var(--dur-tint, 150ms) ease, color var(--dur-tint, 150ms) ease;
}
.ct-ears-mix button:hover { background: var(--tint-hover); }
.ct-ears-mix button[aria-pressed="true"] { background: var(--tint-strong); color: var(--text); }
.ct-ears-mix button:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 2px; }

/* The no-JS pair: both plates, labelled, and the recording as a link. */
.ct-ears-nojs { margin: 0; display: grid; gap: 10px; }
.ct-ears-nojs p { margin: 0; font-size: var(--ct-t-small); color: var(--textQuiet); }
.ct-ears-nojs img { display: block; width: 100%; height: auto; }

/* A NARROW BAND: the gutter goes above the plate, the tick column stays against it. */
@container ct-band (max-width: 540px) {
  .ct-ears { --ears-x: var(--ears-tick); --ears-tick: 30px; }
  .ct-ears-lane {
    grid-template-columns: var(--ears-tick) minmax(0, 1fr);
    grid-template-areas: "gut gut" "tick plot";
    row-gap: 6px;
  }
  .ct-ears-gutter {
    padding-right: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 8px;
  }
  .ct-ears-side, .ct-ears-dev, .ct-ears-model { display: inline; }
  .ct-ears-meter { flex: 1 0 100%; margin-top: 6px; }
  .ct-ears .ct-sono-controls { flex-wrap: wrap; row-gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .ct-ears-lane, .ct-ears-win, .ct-ears-mix button { transition: none; }
}
.ct-ears[data-motion="reduce"] .ct-ears-lane,
.ct-ears[data-motion="reduce"] .ct-ears-win,
.ct-ears[data-motion="reduce"] .ct-ears-mix button { transition: none; }

/* ---- CHECKS: a grid of small verdicts, each with its own evidence ---- */
.ct-checks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 12px;
}
.ct-check {
  padding: 17px 18px;
  border-radius: var(--radius-inner);
  background: var(--tint-rest);
  /* THE DATA ROWS SIT ON THE FLOOR OF THE CARD, so they line up across a row (a lesson the
     Oddities checks paid for): the grid stretches every card to the tallest in its row, each
     card's paragraph is a different length, and without this the lists land on three different
     baselines with a pocket of dead tint under the shortest one. */
  display: flex;
  flex-direction: column;
}
.ct-check-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 9px;
}
.ct-check-head h3,
.ct-check-head h4 {
  margin: 0;
  font-size: var(--ct-t-cardlead);
  font-weight: 600;
  text-wrap: balance;
}
.ct-check p {
  margin: 0;
  font-size: var(--ct-t-note);
  line-height: 1.6;
  color: var(--textQuiet);
  text-wrap: pretty;
}
.ct-check dl {
  /* `auto` top margin takes up whatever slack the stretched card has, which is what puts every
     list in the row on the same line; with no slack it collapses toward the 13px minimum idea via
     the padding below, which still holds the list clear of the hairline. */
  margin: auto 0 0;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 5px 14px;
  font-size: var(--ct-t-caption);
  padding-top: 13px;
  border-top: 1px solid var(--hairline);
}
.ct-check dt { color: var(--textQuiet); }
.ct-check dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
/* Checks sitting BARE on the photograph (an article's checks section under a bare heading) are
   ITEMS on the photograph, so each one takes the shared card recipe: --tint-rest alone is
   invisible over foliage, the exact CANON 18 trap. Inside a sheet or a well the plain
   `.ct-check` above is correct and this modifier is not used. */
.ct-checks--onphoto .ct-check {
  background-color: var(--scrim-hard);
  background-image: linear-gradient(var(--tint-rest), var(--tint-rest));
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 1px 2px rgba(0, 0, 0, 0.28),
    0 10px 28px rgba(0, 0, 0, 0.30);
}
.ct-check-mark {
  font-size: var(--ct-t-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* ---- VERDICT: a stated judgement, its reasoning, the scale it sits on, and room for a figure ---- */
.ct-verdict {
  padding: 20px 22px;
  border-radius: var(--radius-inner);
  background: var(--scrim-soft);
  box-shadow: inset 0 0 0 1px var(--tint-rest);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--ct-verdict-aside, 330px));
  gap: 20px;
  align-items: center;
}
.ct-verdict > :only-child,
.ct-verdict--plain { grid-template-columns: minmax(0, 1fr); }
.ct-verdict-copy { min-width: 0; }
.ct-verdict p {
  margin: 13px 0 0;
  max-width: var(--ct-measure);
  font-size: var(--ct-t-lead);
  line-height: 1.6;
  text-wrap: pretty;
}
.ct-verdict p + p { margin-top: 11px; font-size: var(--ct-t-body); color: var(--textQuiet); }
.ct-badge {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  font-size: var(--ct-t-micro);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--textQuiet);
  /* The label must never split across lines: at 375px a three-word label sat as three fragments
     with the verdict word beside the pair. It wraps as a unit or it stacks. */
  flex-wrap: wrap;
}
.ct-badge .label { white-space: nowrap; }
.ct-badge .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--swiftGreenBright);
  box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.18);
  flex: none;
}
.ct-badge .word { color: var(--swiftGreenBright); letter-spacing: 0.06em; }
/* An aside figure sits at the end of the verdict. `contain` rather than `cover` when the art is a
   cut-out on transparency: the alpha bounding box is the composition, and cropping it clips the
   subject. No tile behind it either, because a rounded box with an inset outline drawn around a
   figure that has no background of its own is a border around empty space. */
.ct-verdict-aside {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: contain;
}

/* ---- GRID: a set of items, usually links. An index, a related list, a set of options ---- */
.ct-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: 16px;
}
/* ONE ITEM MUST LOOK DELIBERATE. An auto-fill grid with a single card leaves it marooned beside
   empty tracks, which reads as a page waiting for content. The feature spans the row and turns its
   art sideways, so the same markup carries an index of one and an index of twenty. */
.ct-grid > .ct-feature { grid-column: 1 / -1; }
.ct-card-art {
  display: block;
  width: 100%;
  /* height:auto so the RATIO BELOW governs whatever the img carries. An <img> width/height pair is
     a presentational hint for the CSS height property, so a card whose art states its intrinsic
     1200x900 was drawing a 900px tall box on a 512px column and ignoring the 5/4 crop this module
     is built on. The generated index emits no width/height, which is why the defect only ever
     showed on hand-authored cards (measured 2026-08-31: 512 by 900 where the module asks for 512
     by 410). Every card that already looked right is unaffected: auto is what it was resolving to. */
  height: auto;
  /* THE CROP IS THE RATIO AND THE SOURCE IS USUALLY SQUARE, so whatever ratio this box takes is
     exactly how much of the subject is thrown away, off the top and bottom where the head and the
     feet are. Keep it NEAR square. Widening it is how the head comes off, whatever the artwork. */
  aspect-ratio: 5 / 4;
  object-fit: cover;
  object-position: center 35%;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
}
.ct-card-body { padding: 16px 18px 20px; }
.ct-card-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 8px;
  font-size: var(--ct-t-micro);
  color: var(--textQuiet);
}
.ct-card-body h3 {
  margin: 0 0 8px;
  font-size: var(--ct-t-cardtitle);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
  text-wrap: balance;
}
.ct-card-body p {
  margin: 0;
  font-size: var(--ct-t-note);
  line-height: 1.6;
  color: var(--textQuiet);
  text-wrap: pretty;
}
/* The card is one big target, so its hover state has to say WHICH part is the link. This is the
   only text that changes, and it changes from the CARD's hover, so the whole card stays clickable
   while only the affordance responds. */
.ct-card-more {
  display: inline-block;
  margin-top: 14px;
  font-size: var(--ct-t-small);
  font-weight: 600;
  color: var(--swiftGreenBright);
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
a.ct-card:hover .ct-card-more,
a.ct-card:focus-visible .ct-card-more { text-decoration: underline; }

/* 900px, NOT 760px, and THE HEIGHT IS STATED WHILE THE WIDTH FOLLOWS FROM IT. Both lessons are
   paid for on the Oddities index (2026-07-28, Paul): a measure-width art column on a SQUARE
   species illustration is exactly how the pheasant's head came off, so the art keeps a near-square
   ratio and takes whatever width that ratio asks for; and below 900px the copy beside a ~432px art
   column wraps taller than the artwork, leaving bare surface under the picture. */
@media (min-width: 900px) {
  .ct-grid > .ct-feature {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: stretch;
  }
  /* A FIXED HEIGHT, because `height: 100%` in an auto-height grid row resolves against content and
     square artwork then drives the whole card to the artwork's height. Width comes from the ratio
     (360 x 6/5 = 432px) and the art crops into it. */
  .ct-grid > .ct-feature .ct-card-art { height: 360px; width: auto; aspect-ratio: 6 / 5; }
  .ct-grid > .ct-feature .ct-card-body { padding: 30px 32px 32px; align-self: center; }
  .ct-grid > .ct-feature h3 { font-size: var(--ct-t-feature); letter-spacing: -0.02em; }
  .ct-grid > .ct-feature .ct-card-body > p { font-size: var(--ct-t-cardlead); max-width: 46ch; }
}

/* ---- SUBJECT: art beside a title block. The opening of an investigation ---- */
.ct-subject {
  display: grid;
  grid-template-columns: minmax(0, var(--ct-subject-art, min(42%, 380px))) minmax(0, 1fr);
  gap: 26px;
  align-items: center;
}
/* HEIGHT: AUTO IS LOAD-BEARING. An <img> width/height pair maps to presentational hints for BOTH
   dimensions, so a rule that sets width alone leaves the height hint standing and a specified
   height makes `aspect-ratio` inert: a 768x614 carving rendered 220 wide by 614 tall, a portrait
   strip with the bird cropped out of both ends (observed on a live page, 2026-09-07).
   With the height released the picture keeps its own shape, so a landscape carving gets a
   landscape slot and a square one a square slot, and no bird is cut. The row is then as tall as
   the taller column and the identity block centres against the art, so neither side leaves a
   void. `contain` only bites where a page pins a ratio of its own: it letterboxes into the recess
   rather than cropping the subject. */
.ct-subject-art {
  width: 100%;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-inner);
  box-shadow: inset 0 0 0 1px var(--imgEdge);
  background: var(--tint-recess);
}
.ct-subject h1,
.ct-subject h2 {
  margin: 0;
  font-size: clamp(26px, 4vw, 31px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
}
.ct-subject-sub {
  margin: 3px 0 15px;
  /* A scientific name, so Fraunces italic (CANON 16, amended 2026-09-07). */
  font-family: var(--fontSpecies);
  font-style: italic;
  font-size: var(--ct-t-h3);
  color: var(--textQuiet);
}
/* A LINE OF TEXT WITH A PILL IN IT, not a flex row. Flex blockified the pill and handed its
   padding back the power to set the row's height, which is the same defect as the flag heading on
   the two-phones page: ONE pill treatment means the container may not undo it. */
.ct-subject-meta {
  margin: 0;
  font-size: var(--ct-t-small);
  line-height: 1.55;
  color: var(--textQuiet);
}
.ct-subject-meta .ct-pill { margin-right: 10px; }

/* ---- ENTRY: a long-form item inside a sheet. The dossier, the profile, the case ---- */
.ct-entry {
  padding: 26px 20px 28px;
  border-top: 1px solid var(--hairline);
}
.ct-entry:first-child { border-top: 0; }
.ct-entry-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px 20px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}
.ct-entry h3 {
  margin: 0;
  font-size: clamp(21px, 3vw, 25px);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.15;
  text-wrap: balance;
}
.ct-entry-sub {
  margin: 0 0 16px;
  font-size: var(--ct-t-small);
  color: var(--textQuiet);
}

/* ============================================================================================
   RESPONSIVE
   ============================================================================================ */

/* MODULE SHAPE IS A CONTAINER QUESTION. Each of these changes on the width of the band it sits in,
   so the same module is correct in a full-width sheet, in a half-width column, and on a phone. */

/* Art beside a title needs about 220px for the art and 40 characters for the title. */
@container ct-band (max-width: 640px) {
  .ct-subject { grid-template-columns: minmax(0, 1fr); }
  /* The art goes FULL WIDTH above the identity. A 260px cap here shrank a landscape carving to a
     postage stamp on a phone, which is the shave-it-down move the design floor rules out. */
  .ct-subject-art { max-width: none; }
}

/* The verdict's aside is a reserve of up to 330px. Below this there is not enough left beside it
   for the copy, so the figure leaves and takes its reserve with it. */
@container ct-band (max-width: 700px) {
  .ct-verdict { grid-template-columns: 1fr; }
  .ct-verdict-aside { max-width: 170px; justify-self: start; }
}

/* The same collapse on viewport width, because a verdict placed straight into a section has no
   .ct-band ancestor and the container query above never fires there: on a phone the aside kept its
   330px reserve and squeezed the copy column to one word per line (observed at 390px, 2026-08-23). */
@media (max-width: 760px) {
  .ct-verdict { grid-template-columns: minmax(0, 1fr); }
  .ct-verdict-aside { max-width: 170px; justify-self: start; }
  /* Same reason for the subject block: outside a band its 640px container query never fires, and at
     390px the art kept its column while the copy wrapped three words at a time. */
  .ct-subject { grid-template-columns: minmax(0, 1fr); }
  .ct-subject-art { max-width: none; }
}

@container ct-band (max-width: 520px) {
  /* A 120px label column plus a number column leaves the bar track about 90px wide, and a 27% bar
     in 90px is 24 pixels of colour trying to carry a comparison. Label and number go above. */
  .ct-bars { grid-template-columns: 1fr auto; gap: 4px 12px; }
  .ct-bars dt { grid-column: 1; }
  .ct-bars dd { grid-column: 2; }
  /* Specificity: `.ct-bars dd` is (0,1,1) and would beat a bare `.ct-track` at (0,1,0), pinning
     the track into the number column. */
  .ct-bars .ct-track { grid-column: 1 / -1; margin-bottom: 10px; }
  .ct-bars .ct-axis { grid-column: 1 / -1; }
  /* Grouped: the 10px breathing room between rows above would undo the grouping here. */
  .ct-bars--grouped .ct-track { margin-bottom: 4px; }
  .ct-bars--grouped .ct-bars-split { height: 16px; }
}

/* The same collapse on viewport width, because a bars list placed straight into a section's prose
   has no .ct-band ancestor and the container query above never fires there: on a phone the six bars
   kept label | track | value in one row and the track measured 149px, so a dashed range band was
   about 12 pixels of hatch (observed at 390px, 2026-08-26). */
@media (max-width: 760px) {
  .ct-bars { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; }
  .ct-bars dt { grid-column: 1; }
  .ct-bars dd { grid-column: 2; }
  /* Specificity: `.ct-bars dd` is (0,1,1) and would beat a bare `.ct-track` at (0,1,0), pinning
     the track into the number column. */
  .ct-bars .ct-track { grid-column: 1 / -1; margin-bottom: 10px; }
  .ct-bars .ct-axis { grid-column: 1 / -1; }
  .ct-bars--grouped .ct-track { margin-bottom: 4px; }
  .ct-bars--grouped .ct-bars-split { height: 16px; }
}

/* And below this a table tile is too narrow for a species name beside its labelled values, so the
   tiles go single file. Above 900px none of this applies: the table is a real table again. */
@container ct-band (max-width: 350px) {
  .ct-data tbody { grid-template-columns: minmax(0, 1fr); }
}

/* THE PAGE SHELL IS THE ONE THING THAT IS GENUINELY A VIEWPORT QUESTION. Page padding, the masthead
   and the space between sections are about the device, not about any module's column.

   THE MOBILE RULE IS TO REMOVE LEVELS, NOT TO SHAVE THEM (CANON 23). Four nested frames each taking
   padding on both sides is what makes a phone's reading column absurd: page 16, sheet 16, band 20,
   inner box 22 leaves 256px of copy on a 412px screen. Shaving each one to 10px keeps four frames
   and still loses 100px. Below 640 the sheet and the well go FULL BLEED and give up their padding
   and their corners, and the verdict gives up its box for a hairline. Two levels instead of four. */
@media (max-width: 640px) {
  .ct-page { padding: 12px 16px 64px; }
  .ct-masthead { margin-bottom: 22px; }
  .ct-masthead-meta { text-align: left; }
  .ct-section { margin-top: 40px; }
  /* The panel-to-panel beat scales with the step it halves. */
  .ct-section:not(:first-of-type):not(:has(> .ct-sectionhead)) { margin-top: 20px; }

  /* -16px is exactly the page's own padding, so these reach the viewport edge and no further.
     A negative margin is a classic overflow cause: this pairing is why it is not one here. */
  .ct-sheet,
  .ct-well {
    margin-left: -16px;
    margin-right: -16px;
    padding: 0;
    border-radius: 0;
  }
  .ct-band, .ct-entry { padding: 22px 16px; }
  /* The pair row's sheets bleed like every other sheet (the pair's `margin: 0` above had kept
     them 16px in, so they stood narrower than the singles round them), and their analysers take
     the band's phone padding. */
  .ct-omni-pair > .ct-sheet { margin-left: -16px; margin-right: -16px; }
  .ct-omni-pair > .ct-sheet > .ct-omni { padding: 22px 16px; }

  /* The hero banner removes its levels with the sheets and wells around it: below 640 it gives up
     its corners and its inset edge and runs to the viewport edge. */
  .ct-hero {
    margin-left: -16px;
    margin-right: -16px;
    border-radius: 0;
    box-shadow: none;
  }

  /* A bled sheet or well hands the reading inset to whatever sits inside it. A band or an entry
     brings its own 16px; anything else (prose, a subject block, a figure) would otherwise put its
     text hard against the viewport edge, which is what 390px showed on 2026-08-23. */
  .ct-sheet > :not(.ct-band):not(.ct-entry),
  .ct-well > :not(.ct-band):not(.ct-entry) {
    padding-left: 16px;
    padding-right: 16px;
  }

  .ct-verdict {
    padding: 18px 0 0;
    background: none;
    box-shadow: none;
    border-radius: 0;
    border-top: 1px solid var(--hairline);
  }
  .ct-check { padding: 15px 16px; }
}

/* ==============================================================================================
   THE COMPARISON MODULE  (cmp-*)  ── shared site furniture
   ==============================================================================================
   Reusable across content pages: a run-day update pulls one species out and sets the two phones
   against it. A generalised descendant of the two-phones head-to-head
   (web/feed/oddities/two-phones/, the .tp-bout--single module), kept here in content.css because
   the noon and evening updates and future pages all reuse it.

   Layout: a centred species header (common name, then scientific name in italic), then a three
   column row treated as three columns for alignment: phone A, the square carving in the MIDDLE
   column, phone B. Each phone column is centred and reads, top to bottom, phone name / recording
   mode / detections / mean-confidence pill / historical-baseline bar.

   The baseline bar (.cmp-base) is the new part. The centre tick is the species' HISTORICAL mean on
   that phone with the default model, and it never moves. Today's value fills out from the tick:
   green (--swiftGreenBright) to the right when today runs hotter than the baseline, orange
   (--obpOrange) to the left when it runs cooler. The half-width of the track is one historical
   inter-day standard deviation, so a fill that reaches the end is a full standard deviation from
   the baseline; --mag (0 to 1, clamp in the markup) is today's distance in units of that half-width.

   Tokens only. The mean-confidence pill reuses .ct-pill and is coloured by /ramp.js (CANON 15). */
.cmp {
  max-width: var(--ct-measure);
  margin: 0 auto;
}
.cmp-head {
  text-align: center;
  margin: 0 0 24px;
}
.cmp-common {
  font-size: var(--ct-t-quote);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.15;
  color: var(--text);
}
.cmp-latin {
  display: block;
  margin-top: 2px;
  /* A scientific name, so Fraunces italic (CANON 16, amended 2026-09-07). */
  font-family: var(--fontSpecies);
  font-style: italic;
  font-size: var(--ct-t-small);
  color: var(--textQuiet);
}
.cmp-row {
  --cmp-cell: 190px;
  display: grid;
  grid-template-columns: repeat(3, var(--cmp-cell));
  justify-content: center;
  align-items: start;
  gap: 40px;
}
.cmp-phone {
  width: var(--cmp-cell);
  min-width: 0;
  text-align: center;
}
.cmp-phone-name {
  display: block;
  font-size: var(--ct-t-lead);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}
.cmp-mode {
  display: block;
  margin-top: 4px;
  font-family: var(--fontUI);
  font-size: var(--ct-t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--textQuiet);
}
.cmp-dets {
  display: block;
  margin-top: 14px;
  font-size: var(--ct-t-small);
  color: var(--textQuiet);
}
.cmp-dets b {
  color: var(--text);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.cmp-mean {
  display: block;
  margin-top: 8px;
  font-size: var(--ct-t-small);
  color: var(--textQuiet);
}
/* The square carving in the middle column, the same edge treatment as every figure on the site:
   pure white at low alpha, never a tinted neutral (a tinted edge reads as dirt along the border). */
.cmp-art {
  display: block;
  width: var(--cmp-cell);
  aspect-ratio: 1;
  /* height:auto is load-bearing, not tidying. The markup carries width="256" height="256" so the
     browser reserves the box before the file lands, and that height attribute maps to an explicit
     height:256px which outranks aspect-ratio: the carving rendered 190x256 and object-fit cropped
     it to a vertical slice (measured live at 1280, 2026-08-27). Handing height back to auto lets
     aspect-ratio do the work at every width. Do not remove it, and do not drop the attributes. */
  height: auto;
  object-fit: cover;
  border-radius: var(--radius-inner);
  box-shadow: inset 0 0 0 1px var(--imgEdge);
  outline: 1px solid var(--imgEdge);
  outline-offset: -1px;
  background: var(--tint-recess);
}
/* The historical-baseline bar. The track is the span, the tick at 50% is the baseline that never
   moves, the fill roots at the tick and reads hotter (green, right) or cooler (orange, left). */
.cmp-base {
  display: block;
  position: relative;
  width: 84%;
  height: 8px;
  margin: 16px auto 0;
  border-radius: 3px;
  background: var(--tint-hover);
}
.cmp-base-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 3px;
}
.cmp-base-fill.is-up {
  left: 50%;
  width: calc(var(--mag, 0) * 50%);
  background: var(--swiftGreenBright);
}
.cmp-base-fill.is-down {
  right: 50%;
  width: calc(var(--mag, 0) * 50%);
  background: var(--obpOrange);
}
.cmp-base-tick {
  position: absolute;
  left: 50%;
  top: -3px;
  bottom: -3px;
  width: 1px;
  background: rgba(250, 250, 245, 0.62);
}
.cmp-base-cap {
  display: block;
  margin-top: 11px;
  font-size: var(--ct-t-micro);
  line-height: 1.45;
  color: var(--textQuiet);
}
.cmp-base-cap b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.cmp-base-cap .cmp-up { color: var(--swiftGreenBright); }
.cmp-base-cap .cmp-down { color: var(--obpOrange); }
.cmp-base-span {
  display: block;
  margin-top: 3px;
  font-size: var(--ct-t-micro);
  color: var(--textQuiet);
}

@media (max-width: 760px) {
  /* THE CARD KEEPS ITS DESKTOP SHAPE ON A PHONE: centred species header, then phone, carving,
     phone across one row (Paul, 2026-08-26). What this defends: the module exists to put two
     devices side by side so the eye compares them without scrolling, and the stack this block
     used to force turned a comparison into three unrelated blocks a screen apart. The columns
     resize instead of stacking. The two device tracks stay equal and cap their content at
     --cmp-cell, so between about 600px and 760px the card looks exactly as it does on a desktop
     with a little more air; below that the tracks shrink and the labels wrap. The carving is the
     part that gives way: it fills whatever its own track is left with, 190px on a tablet and about
     112px on a 390px phone, which keeps the numbers legible and stops the square from setting the
     card's height. The gap is drawn on a straight line from 40px at the 760px breakpoint to 10px at
     390px so nothing jumps as the breakpoint is crossed.
     Do not restore a single column stack here. */
  .cmp-row {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(10px, calc(8.1vw - 21.6px), 40px);
  }
  .cmp-phone { width: auto; max-width: var(--cmp-cell); margin-inline: auto; }
  /* The carving fills its track and keeps the SQUARE the module was drawn for. height:auto is
     repeated here rather than inherited by accident: this block also drops the fixed --cmp-cell
     width for a fluid one, and a 112px wide slot at 256px tall would crop the bird to a vertical
     sliver when the point of the middle column is that you can see it. It starts at the top of
     the row so all three columns share one top line, which is what holds the trio together once
     the carving is shorter than the text beside it. */
  .cmp-art { width: 100%; max-width: var(--cmp-cell); height: auto; align-self: start; margin-inline: auto; }
}

/* ============================================================================
   LIGHTBOX: a still of ANOTHER document, opening that document over the article
   ============================================================================
   WHY IT EXISTS. Some things an article talks about are whole pages of their own, and a pull
   quote out of one loses the thing worth seeing: the typography, the length, the joke of the
   layout. This module shows the top of that page as an ordinary figure, and opens the real page
   over the article when a reader asks for it. The embedded document is untouched, served from its
   own folder, so nothing about it is restyled to fit here.

   WHY A NATIVE <dialog>. showModal() puts the panel in the top layer, so it clears the sticky
   chrome with no z-index race, makes the rest of the page inert, and gives Escape and focus
   return for free. The one thing it does not give is a page that stops scrolling underneath, so
   the script adds .ct-lightbox-lock to the root element and takes it off again on close.

   THE CLOSE BUTTON SITS ABOVE THE FRAME, NOT ON IT. An embedded document brings its own
   background, and a control floated over an unknown one is legible on some pages and gone on
   others. On the backdrop it is legible on all of them. */
.ct-lightbox { margin: 0; }
.ct-lightbox-open {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: zoom-in;
}
.ct-lightbox-open > img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
  outline: 1px solid var(--imgEdge);
  outline-offset: -1px;
}
.ct-lightbox-open:active { scale: 0.995; }
.ct-lightbox-open:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 3px; }
/* The affordance is a word, in the same treatment a card's "more" line uses, so a reader knows
   the still is a door before they click it. It sits ABOVE the image, centred over it: the image
   is then the button's last child, so the button's own box ends where the image ends and the
   clickable area is exactly the image and this line, never a dangling row below the photo. The
   button itself is sized to its widest child (normally the image) rather than the full column,
   so it never claims empty space beside a narrower image either. */
.ct-lightbox-cue {
  display: block;
  margin-bottom: 12px;
  text-align: center;
  font-size: var(--ct-t-small);
  font-weight: 600;
  color: var(--swiftGreenBright);
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}
.ct-lightbox-open:hover .ct-lightbox-cue,
.ct-lightbox-open:focus-visible .ct-lightbox-cue { text-decoration: underline; }

/* THE DIALOG IS SCOPED TO [open]. A bare `display: flex` on the element would beat the user-agent
   `dialog:not([open]) { display: none }` and leave the panel on the page at all times. */
.ct-lightbox-dialog {
  width: min(940px, 100vw - 24px);
  max-width: none;
  height: min(880px, 100dvh - 24px);
  max-height: none;
  padding: 0;
  border: 0;
  background: none;
  overflow: visible;
}
.ct-lightbox-dialog[open] {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* NEARLY BLACK, DELIBERATELY NOT BLACK. The embedded document is a bright page, so the panel only
   reads as its own room when the article behind it is all but gone; a trace of the site's green
   keeps the backdrop from looking like a broken screenshot. */
.ct-lightbox-dialog::backdrop { background: rgba(4, 9, 6, 0.94); }
.ct-lightbox-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.ct-lightbox-label {
  margin: 0;
  font-size: var(--ct-t-small);
  color: rgba(255, 255, 255, 0.72);
}
.ct-lightbox-close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--tint-rest);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
  color: #fff;
  font: inherit;
  font-size: var(--ct-t-small);
  font-weight: 600;
  cursor: pointer;
  transition: background 140ms ease;
}
.ct-lightbox-close:hover { background: rgba(255, 255, 255, 0.14); }
.ct-lightbox-close:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 2px; }
.ct-lightbox-frame {
  flex: 1 1 auto;
  min-height: 0;
  border-radius: var(--radius-inner);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
}
.ct-lightbox-embed {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
html.ct-lightbox-lock { overflow: hidden; }
@media (min-width: 900px) {
  /* THE FRAME HUGS THE DOCUMENT. An embedded page brings its own column width, and a panel
     wider than that column shows the document's backing colour down both sides, which reads
     as dead side panels inside the lightbox. The width here is the embedded paper column
     (49rem) plus the iframe's own scrollbar. A page embedding a different document sets its
     own number; the point is that the number comes from the document, not from the panel.

     AND IT SITS ABOVE CENTRE. A tall desktop window put the panel dead centre with a deep
     band of backdrop above it; the eye starts high on a page of text. The offset only opens
     up once the panel has stopped growing at 880px, so a short laptop window is untouched,
     and the tablet and phone layouts below never see this block at all. */
  .ct-lightbox-dialog {
    width: min(800px, 100vw - 24px);
    margin-block: clamp(12px, calc((100dvh - 880px) * 0.34), 96px) auto;
  }
}
@media (max-width: 520px) {
  /* A phone gives the document every pixel it can: the panel goes to the edges and the bar keeps
     its own line above it. */
  .ct-lightbox-dialog { width: 100vw; height: 100dvh; }
  .ct-lightbox-dialog[open] { padding: 10px; gap: 8px; }
  .ct-lightbox-frame { border-radius: 10px; }
}

/* ---- A PAIR OF CARDS, TWO HALVES ----
   auto-FILL leaves two cards sitting in two of three tracks on a wide column, which reads as a
   grid with a hole in it. auto-FIT collapses the empty track, so a pair always splits the row and
   stacks under about 640px. */
.ct-grid--pair { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }

/* ============================================================================
   PROMPT GRID: the request, and what came back from it
   ============================================================================
   WHY IT EXISTS. A page about how a request is phrased has to put the requests and
   their results next to each other, or the reader is taking the claim on trust. One
   row is one way of asking: a short name for the method, the exact text that went
   out, and the two pictures that came back.

   A ROW IS A ROW, NOT A CARD. Five repeated items inside a well would be five boxes
   inside a box, so the rows are divided by a hairline and nothing else, and the
   module adds no surface of its own.

   THE PROMPT CELL WRAPS. A sent prompt runs to about seven hundred characters and a
   phone column holds forty of them per line, so a cell that scrolled sideways would
   hide most of the thing the row exists to show. overflow-x stays on as the escape
   hatch for an unbreakable run, and this cell is the only element in the module
   allowed to scroll at all.

   THE SHARED TEXT IS QUIET AND THE PART THAT MOVED IS NOT. Every prompt in the grid
   carries the same locked description of the carving. Marking only the difference is
   what turns ten near-identical paragraphs into something a reader can compare, and
   the marking is computed from the source prompts rather than typed in by hand.

   THE PAIR NEVER STACKS. Two pictures from one method are read against each other,
   so they stay side by side at every width; the label and the prompt fall under each
   other instead. */
.ct-vsgrid { display: grid; }
.ct-vsgrid-row {
  display: grid;
  gap: 14px;
  padding-block: 24px;
  border-top: 1px solid var(--hairline);
}
.ct-vsgrid-row:first-child { border-top: 0; padding-top: 0; }
.ct-vsgrid-row:last-child { padding-bottom: 0; }
.ct-vsgrid-label h3 {
  margin: 0;
  font-size: var(--ct-t-h3);
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}
.ct-vsgrid-method {
  margin: 5px 0 0;
  font-size: var(--ct-t-micro);
  line-height: 1.55;
  color: var(--textQuiet);
  text-wrap: pretty;
}
.ct-vsgrid-body { display: grid; gap: 16px; min-width: 0; }
.ct-vsgrid-prompts { display: grid; gap: 14px; min-width: 0; }
.ct-vsgrid-p { min-width: 0; }
.ct-vsgrid-side {
  margin: 0 0 5px;
  font-size: var(--ct-t-micro);
  font-weight: 600;
  color: var(--text);
}
.ct-vsgrid-prompt {
  margin: 0;
  min-width: 0;
  overflow-x: auto;
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", Consolas, "Liberation Mono", monospace;
  font-size: var(--ct-t-micro);
  line-height: 1.7;
  color: var(--textQuiet);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.ct-vsgrid-var { color: var(--text); font-weight: 600; }
.ct-vsgrid-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; min-width: 0; }
.ct-vsgrid-pair figure { margin: 0; min-width: 0; }
.ct-vsgrid-pair img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--imgEdge);
  outline: 1px solid var(--imgEdge);
  outline-offset: -1px;
}
.ct-vsgrid-pair figcaption {
  margin-top: 8px;
  font-size: var(--ct-t-micro);
  line-height: 1.55;
  color: var(--textQuiet);
  text-wrap: pretty;
}
.ct-vsgrid-odds { color: var(--swiftGreenBright); font-weight: 600; }

/* THE ROW STACKS AT EVERY WIDTH, ON PURPOSE. A label column down the left edge was
   drafted and dropped: the widest BAND this template offers measures 676px, so the
   query that would have switched it on could never fire, and taking 148px off the
   prompt cell to gain it would cost the longest element in the module the width it
   most needs. Name, method, prompt, pictures, in reading order, everywhere. */

/* ============================================================================================
   SCROLL REVEAL: the shared entrance for every chart module (Paul, 2026-09-22)
   ============================================================================================
   Driven by /oddities/reveal.js, which adds `is-armed` to a chart root and swaps it for `is-lit`
   the first time the chart scrolls into view; each mark carries its own `--reveal-d` delay. Every
   armed rule is a START STATE that only exists while the script holds the root armed, so no
   JavaScript, no IntersectionObserver or a reduced-motion preference all leave the finished chart.
   Everything sits inside `no-preference` for the same reason.

   THE ANIMATIONS FILL BACKWARDS AND NEVER FORWARDS. The start frame holds through each mark's
   delay, and once a mark has played it drops back to its own resting styles: no clip-path or
   opacity is left standing on a finished chart to crop an overhang or fight a later rule.

   The four entrances, shared by every module file:
     ct-reveal-x     a bar grows from its left end (a clip wipe, so it works on any bar, scaled,
                     offset or patterned, without touching its transform)
     ct-reveal-y     a column grows up from its baseline
     ct-reveal-draw  a line draws itself left to right along its x-axis (the plot is wiped, with
                     room above and below for strokes and markers that overhang the box)
     ct-reveal-pop   a dot, a cell or a day fades and settles into place (the `scale` property, so
                     a mark's own centring transform is left alone) */
@keyframes ct-reveal-x { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes ct-reveal-y { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes ct-reveal-draw {
  from { clip-path: inset(-60% 100% -60% -2%); }
  to { clip-path: inset(-60% -2% -60% -2%); }
}
@keyframes ct-reveal-pop { from { opacity: 0.15; scale: 0.92; } to { opacity: 1; scale: 1; } }

@media (prefers-reduced-motion: no-preference) {
  /* ct-bars: each bar grows from zero to its datum, one after another. */
  .ct-bars.is-armed .ct-bar { clip-path: inset(0 100% 0 0); }
  .ct-bars.is-lit .ct-bar {
    animation: ct-reveal-x 900ms var(--ease-out) var(--reveal-d, 0ms) backwards;
  }
}
