
:root{
  color-scheme:dark;
  --pap:#151716; --ink:#E8E6E0; --dim:#9BA09B;
  --rule:#2B2E2C; --rule2:#232624; --tint:#1C1F1D; --surf:#1E2220;
  --acc:#6FC9AB; --live:#D9B75E; --up:#72CFA4; --down:#D97C74;
  --live-ink:#D9B75E; --kmax:46%;
  --c-crypto:#1B2B30; --c-defi:#1A2B26; --c-markets:#1F2A1D;
  --c-regulation:#2E2320; --c-tech:#21272E; --c-general:#242623;
  --g-crypto:#93D2DC; --g-defi:#96E0C1; --g-markets:#C2E3A4;
  --g-regulation:#EFB89E; --g-tech:#B8CDE2; --g-general:#CBCDC2;

  --bg:var(--pap); --panel:var(--tint); --card:var(--pap);
  --line:var(--rule); --text:var(--ink); --body:var(--ink);
  --muted:var(--dim); --accent:var(--acc); --on-accent:var(--pap);
  --header:var(--pap);
  /* TWO NAMES NINE RULES SAID AND NOTHING DEFINED (batch Q11 §4).
     `var(--chip)` with no declaration anywhere is not a fallback to the
     inherited value — it is the `background` shorthand's INITIAL value, which
     is `transparent`. So eight rules that meant "an inset panel" painted
     nothing at all, and one of them is `.fig-src`, which floats over body
     copy at `z-index:40`: a card you read the paragraph through. `--paper` is
     the same defect with one rule behind it, `.band-new`, a pill that floats
     over the live band.

     Aliases, not new colours. `--chip` is the same tint `.integ` uses through
     `--panel` — the surfaces are the same claim (something set INTO the page
     rather than on top of it) and a second value for it would be a second
     place to drift. `--paper` is the page's own ground, which is what a pill
     floating over moving rows has to be to be legible. Both resolve per
     theme on their own, because `--tint` and `--pap` are declared in both
     palettes; that is the whole reason the alias block exists.

     `check_stylesheet_tokens` is the standing version of this: every
     `var(--x)` the stylesheet uses with no fallback has to be declared in
     it. Mechanical, and it would have caught this the day it was written. */
  --chip:var(--tint); --paper:var(--pap);
  /* Tiers keep their identity and lose their crayons. FLASH is a filled
     oxblood block, DESK the same oxblood outlined, STORY plain ink, DIGEST
     dim — four marks a reader can tell apart without a fifth and sixth hue,
     and none of them amber. The desk's reserved gold was #f0b429, which is
     --live to within a few degrees, and a human column is the opposite of a
     machine reading. */
  --flash:var(--acc); --story:var(--ink); --digest:var(--dim);
  --desk:var(--acc);
  --flash-bg:var(--tint); --story-bg:var(--tint); --digest-bg:var(--tint);
  --accent-bg:var(--tint); --desk-bg:var(--tint);
  /* Elevation is a rule and a tint. There are no shadows, so the variable
     every old rule still names resolves to nothing rather than being hunted
     down one `box-shadow` at a time. */
  --shadow:transparent;

  --ease:cubic-bezier(.2,.6,.2,1);
  /* 4px baseline grid, and a shell that is FLUID WITH A CEILING (R38 §3).
     It was a flat `--wrap:1180px`, so `main` capped at 1220 and a 1920
     display was given a 1220px page with 350px of nothing down each side
     (measured). Every full-bleed surface on this page derives its gutter
     from this one variable — `calc((100% - var(--wrap)) / 2)`, in eleven
     rules — so this is the whole of the outer constraint and the whole of
     the fix.

     THE FLOOR IS TODAY'S NUMBER, on purpose. `clamp`'s lower bound is
     1180px, so at any viewport below ~1340 this resolves to exactly what it
     resolved to before and no phone, tablet or small laptop moves by a
     pixel. The fluid band runs 1340 → 1760; above that the ceiling holds.

     THE CEILING IS 1600px, AND IT IS MEASURED, not chosen. The widest
     surface on the page is the BYTE 100 treemap, and it is authored on a
     1000-unit grid (`_market_map` draws `draw(1000, 395, "wide")`) with its
     smallest tile labels set at 9 of those units. Its rendered scale is the
     carousel card's inner width over 1000, and a browser was asked where
     that reaches 1:1: **shell 2033px** — measured, not derived. Past that
     every label on the map, the 9-unit ones included, is drawn LARGER than
     the size the map was cut at. 1600 puts the map at 0.78x — up from
     0.566x today, which is the 38% the surface was starved by — and leaves
     27% of headroom before it would be upscaled. Unbounded, a 3440 display
     would draw it at 1.62x.

     `100vw` and not `100%`: a custom property's percentage resolves against
     whichever element uses it, and this one is used by eleven different
     elements. The 160px subtracted is 40px more than any scrollbar, so the
     `main` max-width below cannot exceed the viewport on the widths where
     it binds — verified, `documentElement.scrollWidth` is 1920 at 1920 for
     every width in the sweep. */
  --grid:4px; --wrap:clamp(1180px, calc(100vw - 160px), 1600px);
  /* THE SLAB DOES NOT MOVE, and it is the only thing this sizes now (batch
     R51). It was `--river` and it held the card grid too; R51 gave the grid
     the page's content edge (`--wrap`) and Kai kept the slab where it was,
     so a variable called "river" that the river no longer reads would be a
     name describing the wrong surface. Renamed rather than left: the next
     reader should not have to measure the page to find out what it sizes.

     R38 §3's measurement stands for this surface — across shells from 1180
     to 3280 the slab stays 1180.0. */
  --slab:1180px;
  /* The carousel card, as a FRACTION of the shell rather than a pin. It was
     `min(600px,86vw)`: a fixed child inside a fluid parent, which does not
     fill — it leaves a gap and drags the treemap and the movers table down
     with it, because both are cards in this carousel. 0.5085 is 600/1180,
     the ratio the pin already encoded, so the row shows the same 1.92 cards
     at every width and the 600px `max` leaves everything at or below the
     floor exactly as it was. */
  --gc-w:min(86vw, max(600px, calc(var(--wrap) * .5085)));
  /* ── ONE SPACING SCALE (batch J §2) ───────────────────────────────────
     Every vertical gap on this site comes from a step on this scale, and
     every step is a multiple of --grid. An ad-hoc margin is the thing this
     replaces: the page shipped with a 56px void between the instrument
     strip and the first card that belonged to no element and said nothing,
     because nothing anywhere said what a gap was allowed to be.

     The ceiling is deliberate. --s8 is 48px and there is no step above it,
     so a gap over 48px cannot be written as a scale step — it has to be a
     literal, which `check_vertical_rhythm` finds and the batch report has
     to justify. A rule you can satisfy by reaching for a bigger token is
     not a rule.

     Sections are separated by A RULE PLUS ONE STEP (--section-gap), never
     by empty space: a hairline says "a new thing starts here" in 1px, where a
     void says it in 56 and says it worse. */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px;
  --s5:20px; --s6:28px; --s7:36px; --s8:48px;
  --section-gap:var(--s5);
  /* the strip pulls back by a cell's own inline padding so it is the
     LABEL that lands on the page's content edge, not the cell's box */
  --edge-pull:6px;
}
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){
  color-scheme:light;
  /* REDESIGN 2026-09, second pass (Kai: "smooth, soothing for the eye",
     no coral, no blue-black). WARM NEUTRALS: paper a warm off-white, ink a
     warm near-black, the one editorial colour a deep teal-green, gold for
     what is live, and green/brick bound to direction on a figure. `--surf`
     is the raised surface a card and a cell sit on, one step lighter than
     the paper in the light theme and one step lighter in the dark. Every pair below
     is measured in Chrome on the rendered page, both themes (batch report
     §5); the light amber is split as before because amber WORDS need
     4.5:1 and amber MARKS need 3:1. */
  --pap:#F5F4F0; --ink:#1C1F1D; --dim:#5D6561;
  --rule:#E1DFD8; --rule2:#EAE8E1; --tint:#ECEAE4; --surf:#FFFFFF;
  --acc:#1F6F5F; --live:#9A7524; --up:#276F51; --down:#A94A3C;
  --live-ink:#7A5B12; --kmax:72%;
  --c-crypto:#E2EDF0; --c-defi:#E0F0E9; --c-markets:#E6EFDE;
  --c-regulation:#F3E6E0; --c-tech:#E4E9EF; --c-general:#EBEAE5;
  --g-crypto:#93D2DC; --g-defi:#96E0C1; --g-markets:#C2E3A4;
  --g-regulation:#EFB89E; --g-tech:#B8CDE2; --g-general:#CBCDC2;
}}
:root[data-theme="light"]{
  color-scheme:light;
  /* REDESIGN 2026-09, second pass (Kai: "smooth, soothing for the eye",
     no coral, no blue-black). WARM NEUTRALS: paper a warm off-white, ink a
     warm near-black, the one editorial colour a deep teal-green, gold for
     what is live, and green/brick bound to direction on a figure. `--surf`
     is the raised surface a card and a cell sit on, one step lighter than
     the paper in the light theme and one step lighter in the dark. Every pair below
     is measured in Chrome on the rendered page, both themes (batch report
     §5); the light amber is split as before because amber WORDS need
     4.5:1 and amber MARKS need 3:1. */
  --pap:#F5F4F0; --ink:#1C1F1D; --dim:#5D6561;
  --rule:#E1DFD8; --rule2:#EAE8E1; --tint:#ECEAE4; --surf:#FFFFFF;
  --acc:#1F6F5F; --live:#9A7524; --up:#276F51; --down:#A94A3C;
  --live-ink:#7A5B12; --kmax:72%;
  --c-crypto:#E2EDF0; --c-defi:#E0F0E9; --c-markets:#E6EFDE;
  --c-regulation:#F3E6E0; --c-tech:#E4E9EF; --c-general:#EBEAE5;
  --g-crypto:#93D2DC; --g-defi:#96E0C1; --g-markets:#C2E3A4;
  --g-regulation:#EFB89E; --g-tech:#B8CDE2; --g-general:#CBCDC2;
}
:root[data-theme="dark"]{
  color-scheme:dark;
  --pap:#151716; --ink:#E8E6E0; --dim:#9BA09B;
  --rule:#2B2E2C; --rule2:#232624; --tint:#1C1F1D; --surf:#1E2220;
  --acc:#6FC9AB; --live:#D9B75E; --up:#72CFA4; --down:#D97C74;
  --live-ink:#D9B75E; --kmax:46%;
  --c-crypto:#1B2B30; --c-defi:#1A2B26; --c-markets:#1F2A1D;
  --c-regulation:#2E2320; --c-tech:#21272E; --c-general:#242623;
  --g-crypto:#93D2DC; --g-defi:#96E0C1; --g-markets:#C2E3A4;
  --g-regulation:#EFB89E; --g-tech:#B8CDE2; --g-general:#CBCDC2;
}

*{box-sizing:border-box;margin:0}

/* ── Type: TWO roles, two families, nothing else (batch J2 §1) ────────────
   Newsreader carries every word on the site — nav, labels, kickers,
   headlines, standfirsts, body, footer, buttons — and IBM Plex Mono every
   figure (batch K2 §0, which puts back the face J2 §1 swapped out and
   changes neither the count nor the roles). There is no third role: IBM
   Plex Sans is gone with no successor,
   because a newspaper runs ONE text family across the whole paper and a
   second face only for data. The faces are VENDORED (assets/fonts/,
   provenance in PROVENANCE.json) and never hotlinked: a wire that renders its
   headlines only when a third party answers has a dependency it did not
   declare.

   Per EDITION, because coverage is not a matter of taste. Newsreader has no
   Arabic, Bengali, Devanagari or CJK, so the four non-Latin editions do not
   download 189KB of Latin outlines to render a wordmark — they get their
   platform script face for every word, and the mono ALONE, because every
   figure in every edition of this site is a Western numeral. Measured, not
   assumed: 271 ASCII digits and 0 local digits in the intel strip of each of
   ar, bn, hi and zh.

   THERE IS NO ITALIC FILE ANY MORE (batch J3 §3). J2 shipped Newsreader's own
   italic so the label register could be a real one rather than a synthesised
   oblique; J3 retired that register and the second file went with it. No rule
   on this site asks for `font-style:italic` on furniture now, in any edition,
   which is why the four non-Latin editions no longer need a rule putting them
   back to roman — they were the only reader of it. */

@font-face{font-family:'BN Head';font-style:normal;font-weight:400 800;
  font-display:swap;src:url(/f/manrope-latin.woff2) format('woff2')}

@font-face{font-family:'BN Mono';font-style:normal;font-weight:400 700;
  font-display:swap;src:url(/f/jetbrains-mono-latin.woff2) format('woff2')}

body{background:var(--pap);color:var(--ink);font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;
     font-size:17px;line-height:1.5;
     -webkit-text-size-adjust:100%;text-size-adjust:100%;
     /* Every figure on this site is the product, so numerals are
        load-bearing: lining globally, and TABULAR on every figure register
        (`.num` and friends below declare it), so a column of them lines up
        and a countdown does not resize its own tile once a second.
        NOT tabular on the prose face (redesign 2026-09): Schibsted Grotesk
        implements `tnum` by widening its punctuation too, so a body-wide
        tabular setting printed "Shares , Calling" in every headline. */
     font-variant-numeric:lining-nums}
.num,.wnum,.fig,td,th,time,.mono,code,kbd,samp{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
     font-variant-numeric:tabular-nums lining-nums}
nav,button,input,select,textarea,.ui,.lab,.meta,.navlink,.chip{
     font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif}
h1,h2,h3,h4,.hed{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-weight:600;
     line-height:1.15;letter-spacing:-.018em;
     /* §9. A headline wrapped greedily puts as much as it can on line one and
        leaves a word alone on line two — the widow a sub-editor re-writes the
        headline to avoid. `balance` asks the browser to even the lines
        instead, which is the one typographic job that used to cost a person
        an afternoon. Capped at six lines by the spec, which every headline on
        this site is well inside; where a browser does not support it the
        value is ignored and the wrap is what it always was. */
     text-wrap:balance}
/* §9. A FIGURE MAY NOT BREAK ACROSS A LINE. "18.9759 BTC" wrapped after the
   number leaves a bare unit on the next line, and "$1.5M" split at the
   thousands separator is a different number for the length of a blink. Every
   surface that carries a reading gets `nowrap`; the containers around them
   already wrap, so this costs a break opportunity that was never wanted
   rather than an overflow. `.wnum` is deliberately NOT here — a strip figure
   is one line at a fixed 19px with its unit set beside it, and `.wsub` under
   it already ellipsizes, so a `nowrap` on the pair would clip a reading
   rather than protect one. */
.num,.v,.c,time,.stalemark,.tk b{white-space:nowrap}
/* ── TWO FURNITURE REGISTERS, AND THEY MAY NOT LOOK ALIKE (batch J2 §3) ──
   A newspaper distinguishes furniture from furniture by CASE and POSTURE,
   not by reaching for a third family. Both of these are Newsreader now — the
   desk rejected the mono kicker — so what separates them is everything else:

     KICKER   roman, UPPERCASE, letterspaced .14em, small, in --acc.
              `ON-CHAIN · WHALE MOVE · BTC`. It names the story's beat and it
              is editorial, so it takes the editorial colour.
     LABEL    roman, lowercase, untracked, in --dim. `sentiment`,
              `dominance`, `hack tracker · 2026`. It names a COLUMN or a
              CELL — it is the furniture around a reading, not a claim about
              it — so it recedes and it never takes the accent.

   THE LABEL WAS ITALIC AND IS NOT ANY MORE (batch J3 §3). J2 vendored a
   second face to make that italic real; J3 spends the 64,648 bytes back. The
   two registers were never separated by posture in the first place — they
   differ in CASE, in TRACKING, in SIZE and in COLOUR, four ways at once, and
   any one of them is enough to tell a kicker from a label. Dropping the fifth
   costs nothing a reader can lose, and it means no rule on this site asks for
   a style the shipped file cannot draw. That equivalence is what
   `check_furniture_registers` asserts, so it cannot quietly stop being true.

   A BADGE IS NEITHER. FLASH / STORY / DIGEST / DESK is a tier mark with its
   own colour system (see `.badge.flash` below), so it keeps the uppercase and
   the tracking and takes none of the kicker's accent. */
/* THE KICKER IS `.kick`, IT IS MONO, AND IT IS 9.5 (batch K2 §0 and §3).
   Three changes and all three are the drawing's. The NAME is the mock's
   (`.kicker` and `.eyebrow` were two names for one register and neither was
   emitted by anything — the card's kicker is drawn into the SVG, which is why
   a grep of the built tree finds no element carrying either). The FACE is
   Plex Mono, which is §0's ruling for every kicker, label and figure on the
   site. The SIZE is 9.5 with .13em of tracking, down from 12.5/.14em, because
   a kicker is the smallest thing on a card and K's ladder had it larger than
   the meta row under it. */
.kick,.eyebrow{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;text-transform:uppercase;
     letter-spacing:.13em;font-weight:400;font-size:9.5px;color:var(--acc)}
.lab,.colhead,table.data th,table.whales th{
     font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-style:normal;text-transform:lowercase;
     letter-spacing:0;font-weight:400;color:var(--dim)}
.badge{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;text-transform:uppercase;
     letter-spacing:.13em;font-weight:400}

/* ── Geometry ─────────────────────────────────────────────────────────────
   Radius 0. No shadows. No gradients. Newspapers have used hairline rules for
   150 years because a rule carries structure without pretending to be an
   object, and a soft-shadowed rounded card on a light grey field is the house
   style of every AI-built dashboard since 2023. Circles survive only where
   the shape IS the meaning — a status dot. */
*,*::before,*::after{border-radius:0;box-shadow:none}
.dot,.status-dot,[data-shape="dot"],.themebtn .i-sun,.themebtn .i-moon,
.themebtn .i-auto{border-radius:50%}
a{color:var(--acc)}
a:focus-visible,button:focus-visible,input:focus-visible,
select:focus-visible,[tabindex]:focus-visible{
      outline:2px solid var(--acc);outline-offset:2px}

/* ── Header ───────────────────────────────────────────────────────────────
   §3a. A masthead, not an app bar: opaque paper, one hairline under it, and
   no backdrop blur — a blur is a soft effect standing in for a rule, and this
   design has rules. */
/* ── THE HEADER OCCUPIES ONE ROW AT EVERY WIDTH (batch I §11) ─────────────
   It used to say `flex-wrap:wrap`, and that one declaration is why every gate
   stayed green over a masthead that was visibly breaking into four lines:
   `scrollWidth <= clientWidth` is SATISFIED by wrapping. Flex-wrap is
   precisely the browser's mechanism for not overflowing, so 1,134
   measurements passed while `Archive`, the language control and the theme
   toggle sat on a second row at 1440 — and, measured here before the fix,
   FOUR rows at 768–1920, FIVE at 740 (179px of sticky header), and two at
   320 where the menu button dropped below the wordmark.

   `nowrap` makes the row count STRUCTURAL rather than emergent: a header
   that cannot wrap cannot gain a row because content grew. What content
   growth does instead is overflow — which the sweep has always caught, and
   which is now the only failure mode left. The two checks together are
   complete; either alone is the house defect, measuring the property next to
   the one it is named for.

   THE COLLAPSE ORDER IS DECIDED HERE AND WRITTEN DOWN, not left to the
   browser's discretion:

     >= 1200px   wordmark | categories | tier tabs · search · Archive · lang · theme
     721-1199px  wordmark | tier tabs · search · Archive · lang · theme · MENU
                 ^ categories collapse into the overflow menu, which already
                   contains every one of them
     <= 720px    wordmark | search(icon) · theme · MENU
                 ^ Archive and the language control join them in the menu.
                   The search is a magnifier at EVERY width as of
                   2026-08-22, and the tier tabs are retired — see
                   `tools/ui_inventory.py` → RETIRED.

   Only two things may shrink — the categories and the tier tabs — and both
   shrink into their OWN scroll region, never into the document's. */
header{position:sticky;top:0;z-index:5;background:var(--pap);
       border-bottom:1px solid var(--rule);
       padding:8px 20px;display:flex;align-items:center;
       /* `nowrap` as of 2026-08-22, and it is the whole one-row claim.
          Batch I §11 made the second row DESIGNED — an ordered full-width
          break — which answered the accidental half of the complaint and
          left `wrap` in place, so a row was still something the browser
          could add without anyone being told. With the tier tabs retired and
          the search reduced to its magnifier the content fits (959 of
          1,180), so `nowrap` costs nothing today and changes what a future
          overgrowth DOES: it overflows, which the sweep measures, instead of
          quietly becoming a second line, which is the failure this rule
          exists to make impossible. Verified across 2,106 measurements —
          81 widths x 13 pages x 2 modes — one row and no overflow. */
       column-gap:12px;row-gap:0;flex-wrap:nowrap;
       /* G1 §4.1: ONE MEASURE. The masthead and the widget strip are
          full-bleed and the card grid was inset a further ~130px at 1440, so
          the page had two left edges. The RULES still bleed — that is the
          instrument half of the design — and the CONTENT now starts on the
          same line the cards do. Clamped to the 20px gutter (and the notch)
          below the measure, so nothing changes on a phone. */
       padding-inline-start:max(20px,env(safe-area-inset-left),
                                calc((100% - var(--wrap)) / 2));
       padding-inline-end:max(20px,env(safe-area-inset-right),
                              calc((100% - var(--wrap)) / 2))}
/* The wordmark is `bytenews.io`, set in the headline face, IN THE INK — and
   as of batch J2 §5 it wears the mark, which is the one thing the previous
   note said it would not. Two changes and both are the same argument:

     THE .io IS NOT AN ACCENT. `--acc` is EDITORIAL — links, kickers, section
     rules, corrections — and lending it to the second half of our own name
     spends the one colour that means "read this" on the thing a reader is
     never asked to read. It is ink now, and what separates the two halves is
     the weight step the wordmark already has.

     AND IT IS A LOCKUP NOW. "The thing a reader types is the thing the
     masthead says" is still true — the words did not change — but a masthead
     with nothing but its own URL on it has no mark at 16px, in a tab, on a
     home screen or in a share sheet, which is four of the five places a
     brand is actually seen. */
header h1{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:24px;font-weight:800;
       letter-spacing:-.03em;line-height:1;font-variant-numeric:normal}
header h1 a{color:inherit;text-decoration:none}
header h1 b{font-weight:inherit}
header h1 span{color:var(--ink)}
/* Category nav. On a phone it takes a row of its own and scrolls sideways
   rather than wrapping the header to four lines — five links plus the tier
   chips plus search do not fit across 375px on any arrangement. */
/* ── TWO DELIBERATE ROWS, AND THE BREAK IS AN ELEMENT ─────────────────────
   §11 permits a designed second row and forbids an accidental one. This is
   the designed one, and the mechanism is what makes it designed: an empty
   flex item of full width, ordered between the two groups, so the line break
   is a thing in the layout rather than a consequence of running out of room.
   The header cannot gain a THIRD row unless a group overflows, and neither
   group can — every item in row 1 is `flex:0 0 auto` and sums to 322px, and
   the two items in row 2 that could be wide are scroll regions.

   WHY TWO AND NOT ONE, measured rather than argued. The masthead keeps the
   content edge G1 §4.1 gave it, which is `(100% - 1180px) / 2` of padding —
   260px a side at 1440 and 740px at 1920. One row of English needs
   137 (wordmark) + 509 (categories) + 319 (tier tabs) + 150 (search) + 48
   (Archive) + 93 (language) + 44 (theme) + 84 (gaps) = 1,384px of content,
   so it needs 1,644px of viewport at 1440 and 2,124px at 1920. IT HAS NEVER
   FITTED ON ONE ROW AT ANY WIDTH, in any edition, and `flex-wrap:wrap` is
   why nobody was told: wrapping is precisely the browser's mechanism for not
   overflowing, so 1,134 sweep measurements passed over a header that was
   four rows tall.

     row 1   wordmark ............... Archive · language · theme · [menu]
     row 2   categories ............. tier tabs · search

   ONE ROW AS OF 2026-08-22, and the arithmetic above is why it is possible
   now and was not before. The desk asked twice for a single row; what stood
   in the way was 319px of tier tabs and 150px of search field. The tabs are
   RETIRED (see `tools/ui_inventory.py` → RETIRED) and the field is a 44px
   magnifier until it is asked for, so the same sum reads:

     137 (wordmark) + 509 (categories) + 44 (search icon) + 48 (Archive)
     + 93 (language) + 44 (theme) + 84 (gaps) = 959px

   against the 1,180px measure — 221px of slack at 1440, and it is slack
   rather than luck: the editions whose category labels run longest are the
   ones this has to survive, which is why the sweep asserts ONE row on all
   eight rather than on English.

   Opening the search costs 150px inline above 720px (1,109 of 1,180, still
   one row) and nothing at all below it, where the field is an absolutely
   positioned overlay and the row never changes at all.

   THE COLLAPSE ORDER, decided here rather than by the browser:
     >= 1200px  categories visible
     721-1199   categories collapse into the overflow menu (which has carried
                every one of them since the mobile nav shipped)
     <= 720px   Archive and the language control join the menu, and the
                search overlays rather than expanding in flow

   The BREAK ELEMENT IS GONE. It was `header::before` with `flex-basis:100%`
   — an ordered, full-width item that made the second row a thing in the
   layout rather than a consequence of running out of room. With one row
   there is no line to declare, and `flex-wrap:nowrap` below is what keeps it
   that way: the header cannot grow a row, it can only overflow, and the
   sweep measures rows rather than overflow precisely so that a wrap is a
   failure rather than a silent second line. */
header h1{order:1}
header>.cats{order:2}
header>.search{order:3}
header>a.navlink{order:4}
header>.langpick{order:5}
header>.themebtn{order:6}
header>.menubtn{order:7}
/* The utility end. `margin-inline-start:auto` on the FIRST of them pushes
   the whole group to the row's logical end without splitting the free space
   between each pair — and it is on `.search` rather than on `.navlink`
   because the search is the first of the group at every width, while
   Archive and the language control leave for the menu below 720px. */
header>.search{margin-inline-start:auto}
/* Shrinkable, and it shrinks into ITS OWN region. `min-width:0` is what
   allows a flex item to be narrower than its content; without it the row
   cannot be `nowrap` without widening the document. */
/* `flex-basis:0`, and this is the load-bearing part of the two-row header.
   A WRAPPING flex container breaks lines using items' HYPOTHETICAL main
   sizes — their base sizes, before any shrinking — so an item that could
   shrink to fit is put on a new line instead. The full sweep caught it in
   four measurements of 2,106: Spanish and Portuguese category labels are
   longer than English, and at exactly 1200px — the width where the
   categories come back — row 2's base sizes summed past the line and the
   search field was pushed to a THIRD row. English never showed it, which is
   why §11 says assert all eight.
   With a basis of zero this contributes nothing to that sum, takes whatever
   the line has left, and scrolls inside itself when that is not enough. */
.cats{display:flex;align-items:center;gap:0;min-width:0;flex:1 1 0;
      overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none}
.cats::-webkit-scrollbar{display:none}
.cats a{white-space:nowrap}
/* Two groups, one hairline between them — redesign 2026-09. */
.cats .grp{display:flex;align-items:center;gap:10px;flex:0 0 auto}
.cats .grp+.grp{margin-inline-start:10px;padding-inline-start:10px;
      border-inline-start:1px solid var(--rule)}
.search .mag svg,.langpick .i-globe{width:15px;height:15px;display:block}
.langpick .i-globe{margin-inline-end:2px}
.chip-tl .i-tl{width:13px;height:13px;vertical-align:-2px;margin-inline-end:2px}
/* Everything that is NOT one of the two shrinkable regions keeps its size:
   a wordmark that shrinks is a wordmark that has been cut. */
header h1,header>.search,header>a.navlink,header>.langpick,
header>.themebtn,header>.menubtn{flex:0 0 auto}
/* Plain mono labels, not pill buttons. 44x44 is the touch floor (§5c) and it
   is reached with padding rather than with a box, so the control is legible
   as a label and still large enough to hit. */
.themebtn{background:transparent;border:0;color:var(--dim);
      min-width:44px;min-height:44px;padding:0 4px;
      cursor:pointer;display:inline-flex;align-items:center;
      justify-content:center;flex:0 0 auto;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif}
.themebtn:hover{color:var(--acc)}
.themebtn svg{width:16px;height:16px;display:block}
/* Which glyph shows is the CURRENT state, and batch R64 §B1 turned it round
   deliberately. It used to show the mode you would switch TO, which a binary
   toggle can do because "the other one" is a single thing. With three states
   it is not, and the reader's real question changed with it: they have to be
   able to tell Auto from a pinned palette that happens to agree with the
   system, and those two look identical everywhere else on the page.

   KEYED ON THE ATTRIBUTE ALONE — no `prefers-color-scheme` branch. Auto IS
   the absence of the attribute, in both system settings, so the media query
   the old rule needed to work out "the other one" has nothing left to
   decide. */
.themebtn .i-sun,.themebtn .i-moon,.themebtn .i-auto{display:none}
:root:not([data-theme]) .themebtn .i-auto{display:block}
:root[data-theme="light"] .themebtn .i-sun{display:block}
:root[data-theme="dark"] .themebtn .i-moon{display:block}

/* ── Tier filters: a rule-underlined tab row ──────────────────────────────
   §3a. They were rounded pills, which is tell number three. A tab is a label
   with a rule under it; the active one carries a 2px oxblood rule and the
   ink weight, so the state is readable without a fill. The row keeps the
   `.chip` class the client JS and the §0b inventory both name — a rename
   that erases the record is not a rename. */
/* 16.5, the ladder's nav rung (batch K §1). It was 12.5, and the header's
   own "one row at 1280" rule is what pays for the difference — see the gap
   and padding below, both re-measured rather than the header growing. */
.navlink{color:var(--dim);font-size:13.5px;text-decoration:none;
         letter-spacing:-.005em;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-weight:500;
         /* a nav link is a touch target too */
         display:inline-flex;align-items:center;min-height:44px}
.navlink:hover{color:var(--acc)}
.search{display:flex;align-items:center;gap:6px}
/* The in-field close button is a MOBILE control — it belongs to the
   search overlay, which only exists below 720px. Hidden here rather
   than only shown there: the first render of this batch put a stray ✕
   in the desktop header and wrapped it onto a second line. */
.search .clear{display:none}
.search input{background:transparent;border:0;
      border-bottom:1px solid var(--rule);
      color:var(--ink);padding:6px 2px;font-size:12.5px;
      width:150px;outline:none;min-height:44px;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;
      /* NO width transition. Animating the width of an input inside a sticky
         header is what made the mobile search nudge on every keystroke — the
         reflow lands a frame late and the caret moves with it. The overlay
         below is revealed with a transform instead. */
      transition:border-color .18s var(--ease)}
.search input::placeholder{color:var(--dim)}
.search input:focus{border-bottom-color:var(--acc)}
/* THE SEARCH IS A MAGNIFIER AT EVERY WIDTH as of 2026-08-22, which is what
   pays for the one-row header: a 150px field that is empty on arrival buys
   the reader nothing 150px of masthead cannot. It was already the phone's
   behaviour and the JS was already width-agnostic (`toggleSearch` toggles a
   class and focuses); only these two declarations were inside the ≤720px
   block. Above 720 the field then expands IN FLOW beside the icon — the
   absolutely-positioned overlay stays the phone's, where the row has no
   room to grow into. */
.search .mag{display:inline-flex;background:transparent;
      border:1px solid var(--line);
      color:var(--muted);width:30px;height:30px;
      font-size:13px;cursor:pointer;align-items:center;justify-content:center}
.search:not(.open) input{display:none}
/* §2c: the measure caps at 1180px and the field around it stays composed
   above that — the page is centred paper on a paper ground, not a narrow
   site stranded in the middle of an ultrawide screen. */
/* G1 §4.1: the page has ONE content edge, at `max(20px,(100% - wrap)/2)`.
   main's box is the measure PLUS its own two 20px gutters, so its content
   lands on that line rather than 20px inside it — which is what made the card
   grid start 20px to the right of the masthead and the strip. */
/* One scale step top and bottom (batch J §2). It was 24px, which is not a
   step on the 4px scale this design now states, and it stacked with the
   band's own 24px margin to make the 48px void §0 named — a gap that
   belonged to no element and said nothing. The band's margin is gone; this
   padding is the whole of the space between the tape and the first card, and
   the tape's own bottom rule is what says a section ended. */
main{max-width:calc(var(--wrap) + 40px);margin:0 auto;
     padding:var(--section-gap) max(20px,env(safe-area-inset-left))
             var(--s6) max(20px,env(safe-area-inset-right))}
/* Three across, not "as many as fit". The auto-fill track list this replaced
   put four 290px cards on a 1280px container, and at that width a card is a
   thumbnail with a paragraph under it: 16.5px headlines, three-line blurbs,
   nothing with room to be looked at. The counts are explicit rather than
   derived from a min-width because "3 / 2 / 1" is the design, and auto-fill
   reaches 1 column at 820px — a tablet would skip the two-up step entirely.
   `colCount()` reads whichever of these CSS resolved, so the signup band
   still lands at the end of the first row. */
/* `minmax(0,1fr)` and not `1fr`. G1: `1fr` is `minmax(auto,1fr)`, so a track
   cannot shrink below its item's MIN-CONTENT — and on the Spanish front page
   the card's meta row ("resumen · hace 23 h · 7 fuentes" plus the share
   button) has a min-content of 305px, which grew both tracks to 338 inside a
   560px grid and scrolled the whole page sideways at every width from 580 to
   620. English never showed it, which is why the sweep now walks all eight
   editions. */
/* THREE EQUAL COLUMNS (batch K §6). It was six tracks with a lead spanning
   all six and two "second tier" cards spanning three each — and that grid is
   what forced the top of the page to inflate: a card 1,220px wide has to fill
   its box, so its headline went to 44px and its figure to 126, and the rest
   of the page had to whisper underneath. A card in a 1/3 column at 25px needs
   no display type to fill anything. The lead is still the lead — it is FIRST,
   and it is the only card that carries a figure with a display figure in it
   — but it is the same size box as its neighbours, which is what J §1's own
   note said the hierarchy was allowed to be: order and scale, never layout. */
.grid{display:grid;--cols:3;grid-template-columns:repeat(3,minmax(0,1fr));
      gap:0;border-top:1px solid var(--rule);
      /* G1 §4.1: pulled back by a card's own gutter, so it is the card's
         TEXT that lands on the page's content edge rather than its box —
         the same trick the widget strip uses on its cells. Without this the
         card text sat 20px inside the masthead at desktop and flush with it
         on a phone, which is the inconsistency the desk asked to be rid of.
         THE PULLBACK IS GONE (batch K2 §3): a card is a BOX now, so what
         belongs on the page's content edge is the box's own rule, not the
         text inside it, and -20px would hang the leftmost card's border off
         the edge of the page. */
      /* ONE CONTENT EDGE (batch R51, Kai: "make the cards river same width
         as the live band/todays wire, theres like 3 different width size
         right now in one page").

         R38 §3 pinned the river at 1180 while `--wrap` grew, so at 1440 the
         band sat at 1280 and the cards at 1180 — two content edges on one
         page, measured 50px apart. **That pin is reversed here, by the
         desk.** R38's reasoning was about what to spend width ON — "data
         surfaces take the room", "widening the story column is not the win"
         — not that a wider column reads badly, and the prose is capped by
         its own measure anyway, so the box growing does not lengthen a
         line.

         The LEAD SLAB keeps 1180 and is the exception Kai asked for by
         name, which is why `--slab` is now the variable's name: it
         describes the one surface that still uses it. */
      max-width:var(--wrap);margin-inline:auto}
@media (max-width:1100px){.grid{--cols:2;
      grid-template-columns:repeat(2,minmax(0,1fr))}}
/* §5b: single column below 560px, in source order. `minmax(0,1fr)` for the
   same reason the wider rules use it — a bare `1fr` is `minmax(auto,1fr)` and
   the Spanish card's meta row has a 306px min-content, which made the single
   track 298.7px wide inside a 280px grid and scrolled the page sideways at
   320px. */
@media (max-width:560px){.grid{--cols:1;
      grid-template-columns:minmax(0,1fr)}}

/* ── A card is a figure, not an object ────────────────────────────────────
   §3c. It was a tinted, rounded, drop-shadowed box that lifted 3px on hover.
   It is now a column of the page divided by hairlines: the rule IS the
   container. Nothing lifts, because nothing is floating. */
/* DIRECTION A · LIGHT (batch K2 §3). K made the card a column of the page
   divided by hairlines — two rules, no box. The drawing makes it a BOX again,
   and not the tinted rounded shadowed one G threw out: a hairline rectangle,
   16px of padding, 186px of minimum height, and its contents stacked so the
   meta row can be pushed to the foot of it. `min-block-size` rather than
   `height` — a card with four lines of headline is taller and nothing clips.

   THE BORDERS COLLAPSE. The grid is `gap:0` (batch K §6, and it stays), so
   two adjacent cards would draw two 1px rules where the design has one. The
   negative margins pull each card onto its neighbour's edge — the standard
   collapse, and the reason the grid's own `border-top` is still the top rule
   of the first row rather than a doubled one. */
.card{background:var(--pap);
      border:1px solid var(--rule);
      margin-inline-start:-1px;margin-block-start:-1px;
      display:flex;flex-direction:column;
      grid-column:span 1;
      padding:16px;min-block-size:186px;
      transition:background-color .15s var(--ease)}
/* THE PARITY RULES GOT SIMPLER WITH THE GRID (batch K §6, and true again as
   of R45 §2). Every card in this grid is one track wide, so there are no
   special children in front of the uniform run to shift `nth-child`.

   IT SAID THAT ALREADY AND WAS WRONG FOR A WHILE. K §6 removed the spans;
   the 2026-09 redesign put one back — `.grid.has-lead .card.lead` spanned
   two of three tracks above 561px — and this paragraph went on claiming
   otherwise 1,340 lines above the rule that contradicted it. R45 §2 moved
   the lead out of the grid entirely and deleted that rule, which is what
   makes the sentence true rather than what makes it survive. */
/* Nothing here has to change with the column count any more: a collapsed
   border is a collapsed border at three tracks, at two and at one, which is
   what the `nth-child` parity rules that used to live here were doing by
   hand — and getting wrong every time the span count moved. */

/* ── The lead, and the two beside it (batch J §1, regraded by batch K) ────
   Nine cards at one size is a feed with good typography. Three levels is an
   edited page. WHICH story leads is `newsagent/lead.py` — a written rule
   with arithmetic behind it, printed in the ops report — and this block is
   only how the levels are drawn.

   THERE IS NO LEAD CARD ANY MORE (batch R45 §2). It was order AND SCALE AND
   LAYOUT: the lead spanned all six tracks at 44px over a 126px figure, K §6
   took the layout half and K §1 the scale half, and what was left to say
   "this is the lead" was that it came first and carried the page's one
   display figure. R45 takes the figure too — and the card. The lead is a
   SLAB under the price rail (`.lead-slab`), the first thing on the page
   after the ticker, and this block is now only how the SECOND TIER is
   drawn. A signal that has to be inferred from a card's ornament is a
   weaker signal than being first on the page.

   The phone case is still free and for the same reason: the slab is the
   first thing in the markup at every width, so a reader at 390px gets the
   same page and a reader with no CSS at all gets the arrangement as
   document order.

   `.has-lead` STAYS ON THE GRID, and it means something narrower now: not
   "a card in here is the lead" — none is — but "this grid is the page's
   EDITED state", so its first two cards are the second tier and the signup
   band counts from them. `lead.py` decides it, `cardLevel` and
   `_lead_level` read it, `render()` sets the class from the same `led` that
   draws the slab, and the §0b inventory counts it. Kept rather than dropped
   because it still gates real arithmetic in two renderers; if it ever stops
   doing that it goes from the app and the inventory in one commit. */
/* ── THE LEAD SLAB (batch R45 §2) ─────────────────────────────────────
   Four elements between two rules and nothing else: no box, no shadow, no
   radius, no border on three sides. The only frame is the 2px rule it hangs
   from — the drawing's, and the reason the slab needs no card around it.

   LOGICAL PROPERTIES THROUGHOUT. `border-block-start`, `padding-inline`,
   `margin-inline`: this ships in Arabic, and a physical `top`/`right` on a
   divider is exactly the bug `RTL_CSS` exists to catch. The two rules are
   block-direction and the meta row's spread is `justify-content`, so the
   whole thing mirrors without a single direction-specific rule.

   THE HEADLINE IS TWICE A CARD'S AND NEVER MORE. Cards are 17px (batch K2
   §3); this clamps between 24 and 34, which is `2 x 17` at the top and
   still under the masthead's own size at every width. It is fluid because
   the shell is (R38 §3) and a fixed headline in a fluid page is the gap
   that batch removed. `clamp` rather than a media query: one declaration,
   and no breakpoint to keep in step with the shell's.

   The slab is content-width, not full-bleed — it is prose, and the river's
   measure is what running text wants. */
/* THE TOP MARGIN IS `--s2`, NOT `--s5` (batch R47 §4a). The price rail
   carries its own bottom border and the slab opens with a 2px rule; 20px of
   blank between them read as a gap rather than as a start. 8px closes it so
   the rail and the slab read as one unit. The BOTTOM stays at `--s5`,
   because the space under the slab is doing the opposite job. */
.lead-slab{--ls-hd:clamp(24px, 2.1vw + 12px, 34px);
      max-width:var(--slab);margin-inline:auto;
      padding-inline:20px;
      margin-block:var(--s2) var(--s5);
      border-block-start:2px solid var(--ink)}
.lead-slab[hidden]{display:none}
.ls-meta{display:flex;flex-wrap:wrap;gap:6px 14px;
      justify-content:space-between;align-items:baseline;
      padding-block-start:10px;
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;letter-spacing:.12em;
      text-transform:uppercase;color:var(--dim)}
.ls-kick{font-weight:600;color:var(--acc)}
.ls-hd{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-weight:600;
      font-size:var(--ls-hd);line-height:1.14;letter-spacing:-.015em;
      margin-block:10px 0}
.ls-hd a{color:inherit;text-decoration:none}
.ls-hd a:hover{text-decoration:underline;text-underline-offset:3px}
.ls-sum{font-size:17px;line-height:1.5;color:var(--body);
      margin-block:10px 0;max-width:62ch}
/* The dateline's right-hand cell carries two values now (R47 §3) - the
   stamp and the source count - so it gets its own separator. An `<i>` and
   not a `::before`, because the count is conditional and a pseudo-element
   would print a bare pipe on a post with no sources. */
.ls-when i{font-style:normal;opacity:.45;margin-inline:7px}
/* NARROW PHONES DROP THE KICKER TO ITS OWN LINE (batch R47 §3). Measured:
   `08 Sep 2026 · 05:51 UTC | 1 source` does not fit beside the kicker at 360
   or 320, and the dateline wrapped to THREE lines — the slab grew 24.8px at
   both. 390 and up fit on one. So below 390 the row stacks, which is two
   lines instead of three, and the stamp cell itself never wraps: the brief's
   own rule, that a wrapped stamp is worse than a kicker on its own line. */
@media (max-width:389px){
  .ls-meta{flex-direction:column;align-items:flex-start;gap:2px}
  .ls-when{white-space:nowrap}
}
.ls-foot{margin-block-start:14px;padding-block-start:10px;
      border-block-start:1px solid var(--rule2);
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;letter-spacing:.08em;
      color:var(--dim)}
/* THE LIVE LINE (R47 §3). One line, always, at every width from 320 up.
   `min-width:0` on the text plus `overflow:hidden` is what actually lets a
   flex child ellipsis - without it the child's min-content width wins and
   the line wraps, which is the thing this may never do.

   It takes the space the foot's source line vacated, so the slab is still
   four elements and its height should not move; `_LEAD_RECORD` checks that,
   and if it grew the trade did not land. */
/* `padding-block-start:8px`, where the foot it replaced used 10. The line
   it carries is 12.5px body text against the foot's 11px mono, so at equal
   padding the slab grew 2.3px at every width — measured. Two pixels back
   makes the trade cost zero, which is what R47 §3 asked for and what
   `_LEAD_RECORD` records. */
/* IT IS THE BAND'S OWN ROW, SO IT IS SET LIKE ONE (batch R48, Kai: "this
   should be the exact font size and link to the same thing as the one in
   actual live band"). It was 12.5px against the row's 16/15 and clipped to
   a single line, so a phone got half a sentence and an ellipsis. `--bd-fs`
   is the row's size and `1.5` is the row's line-height at both steps
   (22.5/15 and 24/16, measured on the live page) — one declaration each,
   not two numbers kept in step by hand.

   NO `overflow:hidden` AND NO `nowrap` ANY MORE, which is three fixes in
   one property: the sentence wraps to as many lines as it needs, the phone
   gets the whole title, and the pulsing dot stops being cut in half. The
   dot scales to 1.4 (`bnpulse`) about its own centre, so it reached 1.4px
   past the line's left edge and the clip took that half away — Kai saw it
   before any check did. Nothing here needs to clip: the line is the last
   element in the slab and has the slab's padding under it. */
.ls-live{margin-block-start:14px;padding-block-start:8px;
      border-block-start:1px solid var(--rule2);
      display:flex;align-items:baseline;gap:8px;
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;letter-spacing:.08em;
      color:var(--dim);text-decoration:none}
.ls-live b{color:var(--acc);font-weight:600;letter-spacing:.12em;
      text-transform:uppercase;flex:0 0 auto}
.ls-live .age{flex:0 0 auto}
.ls-live .ls-live-t{flex:1 1 auto;min-width:0;color:var(--body);
      letter-spacing:0;font-size:var(--bd-fs,16px);line-height:1.5;
      font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif}
/* A ROW WITH NOWHERE TO GO IS NOT A LINK, and the band says so the same way
   (`.bd:not([data-dest]) .tx{cursor:default}`): the anchor is always in the
   markup so the client can give it a destination when the tape turns over,
   and it carries no `href` until there is one to carry. The arrow is the
   affordance, so it goes with the href rather than standing over nothing. */
.ls-live:not([href]){cursor:default}
.ls-live:not([href]) .ls-arrow{display:none}
/* The dot is a shape and mirrors by sitting in the flex order; the arrow is
   a GLYPH and does not, so it is turned by direction rather than swapped for
   a different character - one element, both editions. */
/* ON THE FIRST LINE'S BASELINE, NOT THE BLOCK'S MIDDLE (batch R48). While
   the sentence could not wrap, `align-self:center` and "beside the LIVE
   label" were the same place. Now that it wraps, centring puts the dot
   halfway down a three-line block — 20.5px below the label, measured — and
   it reads as a bullet for the second line. `baseline` aligns it with the
   label to 0.25px at every width. */
.ls-dot{flex:0 0 auto;inline-size:7px;block-size:7px;border-radius:50%;
      background:var(--acc);align-self:baseline}
.anim .ls-dot{animation:bnpulse 2.4s var(--ease) infinite}
/* INSIDE the sentence as of R48, not beside it: a flex sibling would sit on
   the FIRST line's baseline while the words wrapped past it, leaving the
   arrow stranded top-right. Inline after the last word, it wraps with it. */
.ls-arrow{opacity:.55;margin-inline-start:.35em}
[dir="rtl"] .ls-arrow{transform:scaleX(-1);display:inline-block}
@media (hover:hover){.ls-live:hover .ls-live-t{text-decoration:underline;
      text-underline-offset:3px}
  .ls-live:hover .ls-arrow{opacity:1}}
.card.lead .top{margin-bottom:var(--s4)}
/* ONE HEADLINE SIZE FOR EVERY CARD (batch K §1), AND IT IS THE DRAWING'S
   (batch K2 §3): 17, Newsreader, 600, 1.18, -.01em. It was 44 on the lead, 27
   on the two beside it and 19.5 on the rest before K; K merged them at 25;
   the mock sets `.hd` at 17 in a card that is a 16px-padded box, and a
   headline is 17 at every width now — the phone override K1 kept goes with
   it, because the drawing has one size and a phone is not a different paper.

   `.hd` ON ITS OWN, not `.card .hd`. The weight allowance in §4 names this
   selector exactly, and a rule the allowance can only reach by substring is a
   rule that stops being named the day somebody adds a compound in front of
   it. 600 is the one weight above 400 anywhere on a front-page surface apart
   from `.wnum`, and both are named. */
.hd{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:17px;line-height:1.18;
      letter-spacing:-.01em;font-weight:600;cursor:pointer}
/* The standfirst is one size too — 13.5, the mock's `.lines li`, on every
   card and NOT smaller on a phone. It was 16.5, which is only a point and a
   half under K's 25px headline; against a 17px headline it would have been
   the same size as the thing it sits beneath. */
.card p,.card.lead p{font-size:13.5px;-webkit-line-clamp:3;line-clamp:3}
.card.lead p{max-width:64ch}
.card.lead .pad{gap:var(--s3)}
.card.second p{-webkit-line-clamp:3;line-clamp:3}
/* THE LEAD KEEPS ITS PICTURE ON A PHONE (batch K §0). `.card.lead>.top
   {display:none}` lived here from J4 §1 — the second of two donors bought to
   keep a band cap true — and it took the front page's only image off every
   phone. The cap is retired and so is the donor. */
/* THE PHONE HEADLINE OVERRIDE IS GONE (batch K2 §3, reversing batch K §1).
   K kept 27 on a phone against 25 on a laptop and called it the one thing on
   the page allowed two sizes. The drawing has one `.hd` and this batch
   follows it: band rows, strip labels, the rail, meta, standfirsts and now
   the headline are all the same number at both widths, which is the rule K
   stated and then made one exception to. */
@media (hover:hover){.card:hover{background:var(--tint)}}
.card svg{display:block;width:100%;height:auto}
.card .top{display:block;cursor:pointer;margin-bottom:14px}
.card .pad{padding:0;display:flex;flex-direction:column;gap:9px}
/* 13, the ladder's meta/source rung (batch K §1). It was 11.5.
   THE MOCK'S `.foot` IS NOT THIS ELEMENT and batch K2 §3 did not reach it:
   the drawing puts a 10px mono row under a hairline at the FOOT of the card,
   and this site's meta row is the first thing inside `.pad`, above the
   headline. Moving it is a change to the card's document order, which two
   renderers emit and `check_front_page_renderers` asserts byte-identical —
   more than this batch was asked for. Named here so the gap is on the record
   rather than looking like an oversight. */
.meta{display:flex;gap:10px;align-items:center;font-size:13px;
      color:var(--dim);flex-wrap:wrap;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;
      font-weight:400;min-width:0;overflow-wrap:anywhere}
.meta>*{min-width:0}
/* The tier mark: a kicker, not a crayon. FLASH is a filled oxblood block,
   DESK the same oxblood outlined, STORY plain ink, DIGEST dim — four marks a
   reader tells apart by weight and fill rather than by four more hues, and
   none of them amber, which §2b reserves for things that are moving now. */
.badge{font-size:13px;font-weight:400;letter-spacing:.13em;padding:2px 0;
       text-transform:uppercase;background:transparent;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif}
.badge.flash{background:var(--acc);color:var(--pap);padding:2px 7px}
.badge.story{color:var(--ink)}
.badge.digest{color:var(--dim)}
/* The desk column's badge — the one thing in a card's meta row that says a
   PERSON wrote this. Outlined rather than filled, so it reads as the same
   editorial voice as flash without claiming to be breaking. Its old reserved
   gold was #f0b429, which is --live to within a few degrees, and a human
   column is the opposite of a machine reading. */
.badge.desk{color:var(--acc);border:1px solid var(--acc);padding:1px 6px}
/* The "story so far" chip a card carries when its story has moved. Lives here
   rather than in ARTICLE_CSS because the cards it appears on are on the
   homepage, which does not load that block — the timeline it points AT is the
   part that only article pages carry. Accent-tinted, not tier-coloured: it is
   a property of the story, not another tier. `--accent-bg` is declared with
   the rest of the palette so no literal colour is needed in either theme. */
.chip-tl{font-size:13px;font-weight:400;letter-spacing:.2px;padding:3px 9px;
      background:var(--accent-bg);color:var(--accent);
      text-decoration:none;white-space:nowrap}
.chip-tl:hover{background:var(--accent);color:var(--on-accent)}
/* The live-blog chip. Same footprint, flash-coloured, with a dot that pulses
   where motion is welcome and simply sits still where it is not. */
.chip-live{font-size:13px;font-weight:400;letter-spacing:.6px;padding:3px 9px;
      background:var(--flash-bg);color:var(--flash);
      text-decoration:none;white-space:nowrap;display:inline-flex;
      align-items:center;gap:5px}
/* A STATIC MARK, NOT A PULSE (batch J §3c). It used to breathe at 1.8s, at
   exactly the same rate on a live blog updated a minute ago and on one whose
   last entry was at breakfast — which is the defect §3c exists to remove,
   in miniature. The dot stays because the shape carries the meaning; the
   animation goes because it carried none. */
.chip-live::before{content:"";width:7px;height:7px;
      background:var(--flash)}
/* 19.5px, which a three-across card has the width for. The catchy short title
   is on the graphic above; this is the full one, and it is what a reader
   scans. */
/* the size lives in the lead/second/uniform rule above (batch K §1) */
.card .hd a{color:inherit;text-decoration:none}
.card .hd:hover{color:var(--accent)}
/* The blurb is clamped to two lines by PRESENTATION, not by the writer:
   card_text stays whole in data/posts, in the feed, in the JSON payload and
   in structured data, and only the tile truncates it. A three-line blurb was
   what made the old grid read as a wall — the headline is the thing above it
   that is supposed to carry the story. Both properties: -webkit-line-clamp is
   what every current engine implements, `line-clamp` is the standard name it
   is being renamed to. */
.card p{color:var(--muted);line-height:1.5;
        display:-webkit-box;-webkit-box-orient:vertical;
        -webkit-line-clamp:2;line-clamp:2;overflow:hidden}
.more{display:flex;justify-content:center;padding:28px 0 4px}
/* an author `display` beats the UA sheet's [hidden]{display:none}, so the
   button stayed on screen with its hidden attribute set */
.more[hidden]{display:none}
.more button{background:transparent;border:1px solid var(--line);
      color:var(--text);padding:11px 26px;font-size:14px;
      cursor:pointer}
.more button:hover{border-color:var(--accent);color:var(--accent)}
.more button[disabled]{opacity:.5;cursor:default}
.empty{text-align:center;color:var(--muted);padding:56px 0;font-size:14.5px}
.share{display:flex;gap:8px;margin:18px 0 6px;flex-wrap:wrap}
/* min-width:0 — a flex item defaults to min-width:auto, so the input refuses
   to shrink below its intrinsic size and pushes the row past the viewport */
.share input{flex:1;min-width:0;background:var(--card);
        border:1px solid var(--line);
        color:var(--muted);padding:9px 12px;font-size:12.5px}
.share button,.share a.btn{background:var(--accent);border:0;
        color:var(--on-accent);
        font-weight:400;padding:9px 16px;cursor:pointer;
        font-size:13px;text-decoration:none;display:inline-block}
.share a.alt{background:transparent;border:1px solid var(--line);
        color:var(--muted);font-weight:400}
.share a.alt:hover{border-color:var(--accent);color:var(--accent)}
.srcs{margin-top:14px;font-size:13px;color:var(--muted)}
.srcs a{color:var(--accent);text-decoration:none}
footer{padding:30px 24px 50px;text-align:center;color:var(--muted);font-size:12px;
       line-height:1.9}
footer a{color:var(--muted);text-decoration:underline;text-underline-offset:3px}
/* The diary is the colophon's second sentence, not a second voice: same
   size and same ink, one line of air between them so the reader can tell
   "how this is made" from "how often it changes" without a rule. */
footer .diary{display:inline-block;margin-block-start:6px}
footer a:hover{color:var(--accent)}
/* ── The follow marks (batch R63 §3.2) ────────────────────────────────────
   The platforms' OWN glyphs, at the footer's own ink, sized off the type
   rather than in pixels so they step down with the row on a phone. 1.25em
   against a 12px row is 15px, which is the smallest either mark stays
   legible at.

   `currentColor` and NOT `--acc`: recolouring a trademark to this site's
   brand colour is what the guidelines prohibit, and it would read as an
   endorsement the platforms have not given. The hover follows the row's own
   hover for the same reason — it is the LINK reacting, not the mark. */
/* ── The header's own strip — batch R64 §B2 ───────────────────────────────
   `order:3` and not a number of its own: `.search` is already 3 and equal
   orders keep DOM order, so the marks land immediately after the magnifier
   without renumbering the four items behind it.

   MEASURED BEFORE IT WAS WRITTEN, because the brief's worry was the
   1100-1220 band. That band is where `.cats` is ALREADY GONE — it is
   hidden at 1199 — so the tight case is 1200 exactly, and there the nav is
   allocated 657px for 455px of content. 202px of slack against the 92px
   two marks and their gaps cost, on bytenews.io, measured in the browser.
   Below 1200 the strip is not drawn at all: the panel carries the accounts
   there, and the phone header keeps exactly the three controls it had. */
header>.socs{order:3;gap:8px;margin-inline-start:2px}
header>.socs a.soc{color:var(--dim);padding:0 2px}
header>.socs a.soc:hover{color:var(--acc)}
@media (max-width:1199px){header>.socs{display:none}}

.socs{display:inline-flex;align-items:center;gap:10px;
      vertical-align:-.2em;margin-inline-end:2px}
.socs a.soc{display:inline-flex;text-decoration:none;border-bottom:0;
      padding:0;line-height:0}
.socg{inline-size:1.25em;block-size:1.25em;fill:currentColor}
/* ── Mobile navigation ────────────────────────────────────────────────────
   Above the breakpoint none of this applies and the header is exactly what
   it was. Below it the header keeps only what a thumb needs — the brand, the
   search icon, the theme toggle and the menu button — and everything else
   moves into a panel.

   The panel is a child of <header>, positioned `absolute; top:100%`. The
   header is sticky (a positioned element and, via backdrop-filter, a
   containing block), so that anchors the panel to the bottom of the header
   at any header height, scrolled anywhere on the page, with no JS measuring
   and no magic number to drift. Being out of flow it also cannot move the
   header's own contents: opening the menu overlays the page rather than
   pushing it down. */
/* ── Language switcher ──────────────────────────────────────────────────────
   A <details>, so it opens and closes with no JavaScript at all and still
   works with the panel script that owns Escape on a phone. Every entry is a
   real link to the SAME page in another edition, which is also what the
   hreflang alternates in <head> say — one set of URLs, stated twice for two
   different readers. */
.langpick{position:relative;flex:0 0 auto}
.langpick>summary{list-style:none;cursor:pointer;border:0;
      color:var(--dim);padding:0 6px;font-size:12px;min-height:44px;
      font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;text-transform:uppercase;letter-spacing:.1em;
      display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.langpick>summary::-webkit-details-marker{display:none}
.langpick>summary:hover,.langpick[open]>summary{color:var(--acc)}
.langmenu{position:absolute;top:calc(100% + 7px);inset-inline-end:0;z-index:12;
      background:var(--panel);border:1px solid var(--line);
      padding:6px;min-width:152px;display:flex;flex-direction:column;
      }
.langmenu a{padding:7px 11px;font-size:13.5px;
      text-decoration:none;color:var(--text);white-space:nowrap}
.langmenu a:hover{background:var(--tint);color:var(--acc)}
.langmenu a[aria-current="true"]{color:var(--accent);font-weight:400}
.menubtn,.navpanel,.navscrim{display:none}
/* ── 721-1199: the first documented collapse ──────────────────────────────
   Eight category links, four tier tabs, a search field, Archive, a language
   control and a theme toggle sum past the viewport well before the phone
   breakpoint — measured at 1,424px for the English edition, which is sixteen
   pixels of headroom at 1440 and none at all in German. The categories go
   first because they are the only items with somewhere to go: the overflow
   panel has carried every one of them since the mobile nav shipped. */
@media (max-width:1199px){
  .cats{display:none}
  .menubtn{display:inline-flex;background:transparent;
        border:1px solid var(--rule);color:var(--dim);
        min-width:44px;min-height:44px;padding:0;cursor:pointer;
        align-items:center;justify-content:center;flex:0 0 auto}
  .menubtn:hover{border-color:var(--acc);color:var(--acc)}
  .menubtn svg{width:16px;height:16px;display:block}
  .menubtn .i-x{display:none}
  .menubtn[aria-expanded="true"]{border-color:var(--acc);color:var(--acc)}
  .menubtn[aria-expanded="true"] .i-bars{display:none}
  .menubtn[aria-expanded="true"] .i-x{display:block}
  /* THE THREE BARS. These lived in the 720 block, and promoting the button
     to 1199 without them rendered an EMPTY BORDERED SQUARE between 721 and
     1199 on every page. Caught by LOOKING at the artifact, not by a gate:
     the button was present, focusable, 44x44 and correctly labelled, so
     every check that could see it passed.
     Centred on the button rather than pinned 13px from its edge — the
     button is `min-width:44px` here and was a fixed 30px there, so the old
     offset put the bars off-centre at the new size. */
  .menubtn{position:relative}
  .menubtn svg{display:none}
  .menubtn i{position:absolute;left:50%;margin-left:-9px;top:50%;
        width:18px;height:2px;background:currentColor;
        transition:transform .2s var(--ease),opacity .12s linear}
  .menubtn i:nth-child(1){margin-top:-7px}
  .menubtn i:nth-child(2){margin-top:-1px}
  .menubtn i:nth-child(3){margin-top:5px}
  .menubtn[aria-expanded="true"] i:nth-child(1){
        transform:translateY(6px) rotate(45deg)}
  .menubtn[aria-expanded="true"] i:nth-child(2){opacity:0}
  .menubtn[aria-expanded="true"] i:nth-child(3){
        transform:translateY(-6px) rotate(-45deg)}
  .navscrim{display:block;position:absolute;top:100%;left:0;right:0;
        height:100vh;background:rgba(0,0,0,.45)}
  .navpanel{display:flex;flex-direction:column;position:absolute;
        top:calc(100% + 1px);left:0;right:0;background:var(--pap);
        border-bottom:1px solid var(--rule);padding:6px 16px 14px}
  .navpanel[hidden],.navscrim[hidden]{display:none}
  .navpanel a{color:var(--ink);text-decoration:none;font-size:15px;
        padding:11px 4px;border-bottom:1px solid var(--rule2);
        letter-spacing:.2px}
  .navpanel a:last-child{border-bottom:0}
  .navpanel .grp{display:flex;flex-direction:column}
  .navpanel .grp+.grp{border-top:1px solid var(--rule);margin-top:4px;
        padding-top:4px}
  .navpanel a:hover,.navpanel a:focus{color:var(--acc)}
}
@media (max-width:720px){
  /* ONE ROW. The break is removed rather than hidden — a zero-height flex
     item with `flex-basis:100%` still forces a line, so `display:none` is
     the only thing that actually collapses the two rows into one. */
  header::before{display:none}
  header{padding:11px 16px;column-gap:9px}
  /* At 320 the row is wordmark(133) + magnifier(44) + theme(44) + menu(44)
     = 265, plus three 9px gaps and 32px of gutter = 324. Four pixels over,
     and a wordmark may not be cut (§10.1) — so the gutter is what gives,
     not the brand. */
  /* The brand takes all the slack, so the right-hand controls sit against
     the edge whatever the page happens to carry. Doing it with margin-left
     on the controls means splitting the free space when a page has two of
     them — the bug the old `:first-of-type` rule was working around. */
  header h1{font-size:17px;letter-spacing:2.6px;margin-inline-end:auto}
  /* Category links, Archive and About are in the panel now. */
  .cats,header>a.navlink{display:none}
  .search{margin-inline-start:0}
  /* Closed, the input is not there at all — the magnifier is the control.
     `display:none` rather than a zero width: a zero-width input is still a
     flex item, and it was the thing being animated. */
  .search:not(.open) input{display:none}
  .search .mag{display:inline-flex}
  .menubtn{display:inline-flex;background:transparent;
        border:1px solid var(--line);color:var(--muted);
        width:30px;height:30px;padding:0;cursor:pointer;align-items:center;
        justify-content:center;flex:0 0 auto}
  .menubtn:hover{border-color:var(--accent);color:var(--accent)}
  .menubtn svg{width:16px;height:16px;display:block}
  .menubtn .i-x{display:none}
  .menubtn[aria-expanded="true"]{border-color:var(--accent);
        color:var(--accent)}
  .menubtn[aria-expanded="true"] .i-bars{display:none}
  .menubtn[aria-expanded="true"] .i-x{display:block}
  .navscrim{display:block;position:absolute;top:100%;left:0;right:0;
        height:100vh;background:rgba(0,0,0,.45)}
  /* No `animation:` here, deliberately — see MOBILE_CSS. The panel opens more
     than once, and an entrance animation is the wrong mechanism for a thing
     that does: the open state has to be something the cascade says, not the
     residue of an animation that has to re-run every time. */
  .navpanel{display:flex;flex-direction:column;position:absolute;
        top:calc(100% + 1px);left:0;right:0;background:var(--panel);
        border-bottom:1px solid var(--line);padding:6px 16px 14px;
        }
  /* An author `display` beats the UA sheet's [hidden]{display:none} — the
     same trap the Load more button fell into. */
  .navpanel[hidden],.navscrim[hidden]{display:none}
  .navpanel a{color:var(--text);text-decoration:none;font-size:15px;
        padding:11px 4px;border-bottom:1px solid var(--line);
        letter-spacing:.2px}
  .navpanel a:last-child{border-bottom:0}
  .navpanel .grp{display:flex;flex-direction:column}
  .navpanel .grp+.grp{border-top:1px solid var(--rule);margin-top:4px;
        padding-top:4px}
  .navpanel a:hover,.navpanel a:focus{color:var(--accent)}
  /* Tier chips are grid controls, not navigation, so they are not in the
     panel: navjs moves the same element into the grid's tool row and back
     as the breakpoint is crossed. One row, scrolled sideways — wrapped, four
     chips cost the grid a second line of vertical space above the fold.
     The padding is NOT touched here: it used to set `padding-bottom:2px`
     alone, which left the track 3px above its chips and 2px below them and
     put the glider — inset by the same token on both edges — a pixel short
     of the chip it sits behind. Even padding is one value, in MOBILE_CSS. */
  header>.langpick{display:none}
  .navpanel .langrow{display:flex;flex-wrap:wrap;gap:7px;
        padding:12px 0 2px;margin-top:4px;border-top:1px solid var(--line)}
  .navpanel .langrow a{padding:6px 11px;border:1px solid var(--line);
        font-size:13px;border-bottom:1px solid var(--line)}
  .navpanel .langrow a[aria-current="true"]{border-color:var(--accent);
        color:var(--accent)}
}
/* At 320 the header row is wordmark(132.9) + magnifier(44) + theme(44) +
   menu(44) = 264.9 of content, and with a 9px gap and the phone's 16px
   gutter it needs 323.9px against 320. THREE POINT NINE PIXELS, in every one
   of the eight editions — the wordmark is the same Latin string in all of
   them — and the header wrapped to a second row for want of it.

   The GAP is what gives. Not the gutter, which is the content edge the
   wordmark shares with the first tile's label and the card headline (G1
   §4.1); not the wordmark, which may not be cut (§10.1); and not the three
   buttons, which are already at the 44x44 touch floor. 6px leaves 5.1px. */
@media (max-width:360px){
  header{column-gap:6px}
}

/* ── The lockup ───────────────────────────────────────────────────────────
   The mark sits at 0.80em with a +0.02em optical nudge and a 0.38em gap.
   Every one of those is a RATIO to the wordmark rather than a pixel, so the
   lockup holds together at the masthead's 23px and at a specimen's 96px
   without a second rule — and the 14px floor is the one absolute. The floor
   SURVIVED batch R63 and its reason did not: it used to be the 1.5-unit
   stroke on the unset cells. On a 13-unit grid 14px puts one unit at
   1.08px, and the thinnest thing the cut b has is the 1-unit STEM — at the
   floor it is a single device pixel, and below it the stem drops out, the
   b loses its ascender, and what is left is a block with a notch in it.

   THE NUDGE IS DOWNWARD AND IT IS NOT A FUDGE. A geometric square aligned to
   a baseline sits optically HIGH beside lowercase letterforms, because the
   baseline is where the letters' round overshoot ends and the square's flat
   edge does not overshoot at all. 0.02em is the correction, measured against
   the x-height of Newsreader at the masthead size.

   LOGICAL, NEVER MIRRORED. The gap is `margin-inline-end` so it follows the
   writing direction — but the lockup itself is `dir="ltr"` in all eight
   editions, because `bytenews.io` is a Latin domain name and a mirrored
   lockup would put the mark after a name that still reads left to right. */
header h1 a{display:inline-flex;align-items:baseline}
.mk-byte{inline-size:.8em;block-size:.8em;
      min-inline-size:14px;min-block-size:14px;
      margin-inline-end:.38em;flex:0 0 auto;
      /* baseline-aligned and then nudged; `align-self` is what puts a
         replaced element's own baseline where the text's is */
      align-self:baseline;position:relative;top:.02em}

/* ── The two fields ───────────────────────────────────────────────────────
   NOT `currentColor`, and that is the mechanism change batch R63 made. The
   four-cell mark that stood here was one colour throughout — a tile in
   `currentColor` with cells knocked out of it — so it could inherit the ink
   of whatever it sat in, and the rule above deliberately set no `color` so
   that it would. The cut b is TWO FIELDS by construction, and no single
   `color` expresses a paper field and an accent field at once.

   So the paint is named, out of the stylesheet's own `:root`: `--acc` for
   the field that must dominate, `--pap` for the field that is the page. Both
   flip with the theme, which is what the pack's own `mark.css` hardcodes a
   media query for and what this does not have to. THE ACCENT SIDE IS THE
   5/8 SIDE and that is the pack's one rule — paper on the 5/8 side is the
   page's own colour, and the masthead becomes a green b beside a green
   block. */
.mk-byte .f{fill:var(--acc)}
.mk-byte .p{fill:var(--pap)}

:root{--ease:cubic-bezier(.2,.8,.2,1)}
/* THE kill switch. One block, and selfcheck greps for it: everything above
   is decoration, and a reader who asked for less of it gets none of it. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms !important;
        animation-iteration-count:1 !important;
        transition-duration:.001ms !important;scroll-behavior:auto !important}
  /* Reveals are INSTANT for this reader, not merely fast: the hidden state
     never applies, so there is no frame in which a card is invisible and no
     observer the page depends on to bring it back. `!important` because this
     block is above the entrance rules and they are the same specificity. */
  .js .card{opacity:1 !important;transform:none !important}
}
/* Entrance. The hidden state hangs off `.js` — a script is running — and NOT
   off `.anim`: motion is a separate question, answered above and in bnObserve.
   With JS off, and for a crawler, neither class is on <html> and every card is
   simply visible.
   `.in` is one-way: a card that has been revealed is never animated again, so
   a filter toggle and a back-navigation are instant. */
.js .card{opacity:0;transform:translateY(14px);
        transition:opacity .4s var(--ease),transform .4s var(--ease),
        border-color .2s var(--ease)}
.anim .wcell{opacity:0;transform:translateY(12px);
        transition:opacity .28s var(--ease),transform .28s var(--ease),
        border-color .2s var(--ease)}
.js .card.in,.anim .wcell.in{opacity:1;transform:none}
/* Arrive with no animation at all: a card that was already on screen when the
   observer first saw it (the reveal would be a fade over something the reader
   is looking at), and every card the grid rebuilds for a filter toggle.
   `now` is worn for ONE FRAME and taken off again (see MOTION_JS's `snap`).
   At 0,3,0 it outranks the card's own `transition` (0,1,0) — the hover lift
   and the theme swap's cross-fade both ride on that one declaration — so a
   card that kept the class would keep neither, and after one filter toggle
   that is every card on screen. */
.js .card.now{transition:none}
/* ── Motion from the data (redesign 2026-09) ─────────────────────────
   The strip's bars GROW to their reading as the cell is revealed — the fill
   is the figure, the growth is it arriving. The head's 48 bars draw up in
   sequence on load. The mark in the masthead TICKS — its two set bits swap
   with its two unset ones — each time the poll lands a row (MOTION_JS
   listens for `bn:arrive`), which is the one moving thing on the page that
   moves only when the wire does. And the lockup eases down as the page
   scrolls under the masthead, on the scroll timeline where the browser has
   one. Transform, opacity and fill only; every rule is behind `.anim`. */
.anim .wcell .wbar i{transform:scaleX(0);transform-origin:left center;
      transition:transform .9s var(--ease) .15s}
[dir="rtl"] .anim .wcell .wbar i{transform-origin:right center}
.anim .wcell.in .wbar i{transform:scaleX(1)}
.anim .band-hist rect{transform-origin:center bottom;
      animation:bnbar .5s var(--ease) both;
      animation-delay:calc(var(--i,0) * 14ms)}
@keyframes bnbar{from{transform:scaleY(0)}to{transform:scaleY(1)}}
/* A row landed: the mark's two fields TRADE PLACES for 700ms (the JS that
   adds the class is unchanged, and so is the 700ms). This replaces an
   opacity inversion on lit and unlit cells, which has no equivalent on a
   two-field mark — there is nothing here to dim. `fill` rather than
   `opacity` because a half-lit field is not a state this drawing has. */
.mk-byte rect{transition:fill .18s var(--ease)}
.mk-byte.tick .f{fill:var(--pap)}
.mk-byte.tick .p{fill:var(--acc)}
@supports (animation-timeline:scroll()){
  .anim header h1 a{animation:bnmast linear both;
      animation-timeline:scroll();animation-range:0 160px;
      transform-origin:left center}
  [dir="rtl"] .anim header h1 a{transform-origin:right center}
  @keyframes bnmast{to{transform:scale(.86)}}
}
/* Offscreen loops are parked. An infinite animation the reader cannot see
   still costs a frame budget the scroll needs — the marquee and the pulses
   are the two that run forever. Applied by bnObserve's second observer, and
   deliberately not behind `.anim`: pausing is never the wrong answer.
   The PSEUDO-elements are named explicitly, and that is the whole rule
   working rather than half of it: both pulses are `::before`s, `*` matches
   elements and never a pseudo-element, and `animation-play-state` is not
   inherited — so the first version of this rule parked the marquee and left
   the dots spinning, once per breaking card, while the reader scrolled. */
.offscreen,.offscreen *,
.offscreen::before,.offscreen *::before,
.offscreen::after,.offscreen *::after{animation-play-state:paused !important}
/* Hover: the lift the cards already had, plus a very slight scale on the
   graphic inside its own clipped box. Touch devices get none of it. */
.card .top{overflow:hidden}
.card .top svg{transition:transform .3s var(--ease)}
@media (hover:hover){
  .card:hover .top svg{transform:scale(1.02)}
}
/* Breaking. A pulse, not a siren: 1 → 1.35 on a 6px dot with the glow doing
   most of the work, on a 1.6s loop. The glow is STATIC and the scale rides on
   top of it — it used to be two box-shadow keyframes, which is a repaint on
   every frame of an infinite loop, once per breaking card in the grid: jank
   you feel in a scroll rather than see in the dot. Transform and opacity
   only, everywhere. */
.badge.flash::before,.chip-live::before{content:"";width:6px;height:6px;
      border-radius:50%;background:currentColor;display:inline-block;
      margin-inline-end:6px;vertical-align:middle}
/* NOTHING ON THIS SITE PULSES ANY MORE (batch J §3c). `bnpulse` drove the
   FLASH badge's dot, the live-blog chip's and the recent-hack dot, and all
   three pulsed at 1.6s whether the thing they marked had happened four
   minutes or fourteen hours ago. A FLASH card already prints its age two
   millimetres to the right — which is the number §3c asks for, and it was
   already there, being talked over by a dot.

   The dots survive as static marks. THEME_CSS allows a circle exactly where
   the shape IS the meaning, and a bullet before a tier word is that; what it
   does not allow is a shape pretending to be a reading. */
/* THE BREATHING DOT IS GONE (batch J §3c). It was the one always-moving
   element in the strip and it moved at exactly the same rate whether the gas
   feed had answered in the last minute or had been dead since Tuesday. Every
   status dot on this site is an age now — see AGE_CSS — and an age cannot be
   faked by a keyframe, which is the entire argument. */
/* A hack inside the last day keeps its dot and loses its pulse — same rule,
   and the tile already prints the date. */
/* A COUNTDOWN INSIDE ITS LAST HOUR NO LONGER FLASHES (batch J §3d). It
   animated to `--accent`, which is oxblood, which is EDITORIAL — links,
   kickers, corrections, FLASH — and lending it to a machine state is the
   same mistake as the amber above. The countdown is already counting, which
   is §3c's whole argument for a number over a light. */
/* The new-bytes pill drops in with a small overshoot. */
.anim #pill:not([hidden]){animation:bndrop .32s var(--ease)}
@keyframes bndrop{
  0%{transform:translate(-50%,-14px);opacity:0}
  70%{transform:translate(-50%,3px);opacity:1}
  100%{transform:translate(-50%,0)}
}
/* A PRINT: a price that has just moved. Applied by the bar's own paint(),
   and only where the NUMBER changed — not on every repaint. See TICKER_HTML.

   NOT CALLED `flash`, deliberately. `.badge.flash` is the breaking-news tier
   — a filled oxblood block with its own colour system — and `--flash` is a
   token. A price tick sharing that name collides in the CSS, in the strings
   and in a reader's head. A terminal calls this a print and so does this.

   TWO ANIMATIONS, NOT ONE, and that is the whole of "colour is not the
   signal". It replaced a single neutral `bnbump` wash on `--accent` that was
   identical up and down, so the animation carried no information at all and
   the direction lived only in the `.c` cell's sign. Around 8% of men cannot
   reliably separate `--up` from `--acc`; the colour is emphasis on a
   direction the row already states as `+1.24%` with an `up`/`down` class,
   and a reader who cannot see the hue loses nothing but the emphasis.

   Colour ONLY — nothing in the bar may change size while it is scrolling. */
.tk .v.print-up{animation:bnprint-up .5s var(--ease)}
.tk .v.print-down{animation:bnprint-down .5s var(--ease)}
@keyframes bnprint-up{0%{color:var(--up)}100%{color:inherit}}
@keyframes bnprint-down{0%{color:var(--down)}100%{color:inherit}}
/* The bar's own age, at the note's size rather than the age element's own
   13px. `.age` is reused rather than reimplemented so `AGE_JS` counts it for
   free — one interval for the page — and so it inherits the reserved `4ch`
   box that stops a number changing once a second from resizing the track. */
/* The reaction panel — batch Q3 §1. A key/value list, not a table: the
   fields are independent readings and a table would imply they are columns
   of one thing. `.rxu` is the UNMEASURED rendering and is deliberately not
   a dash or a zero — it carries a word, because a blank cell and a measured
   zero must not look alike. */
.rxpanel{border-top:1px solid var(--line);margin:28px 0 0;padding:18px 0 0}
.rxpanel h2{font-size:13px;letter-spacing:.13em;text-transform:uppercase;
      color:var(--dim);margin:0 0 12px;font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif}
.rx{display:flex;align-items:baseline;gap:10px;padding:5px 0;
      border-bottom:1px solid var(--line);flex-wrap:wrap}
.rx:last-of-type{border-bottom:0}
.rxk{flex:1 1 40%;color:var(--dim);font-size:13px}
.rxv{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;
      color:var(--text);font-size:14px}
.rxn{color:var(--muted);font-size:12px;opacity:.8}
/* NOT ITALIC. This site ships one face in one style and no italic
   `@font-face`, so `font-style:italic` would be a SYNTHESISED
   oblique — the shipped face cannot draw one and the browser would
   fake it. `check_no_synthesised_oblique` refuses that, correctly,
   and it caught this line. The distinction this rendering needs is
   carried by the WORD anyway: a blank cell and a measured zero must
   not look alike, and a word versus a number is already that. */
.rxu{color:var(--muted);font-size:13px;letter-spacing:.02em}
.tk-note .age b,.tk-note .age .what{font-size:11px}
.tk-note .age b{color:var(--muted);min-width:3ch}
/* The theme swap, and nothing during first paint — THEME_INIT runs before
   the stylesheet, so there is no transition to catch a flash with. */
body,header,.card,.wcell,.chip{transition:background-color .15s linear,
      color .15s linear,border-color .15s linear}

/* ── Desktop header, alive in place ─────────────────────────────────────────
   The LAYOUT is untouched — Kaisar is happy with it and selfcheck asserts the
   1280px box positions are unchanged. What is added is behaviour: a sliding
   underline coloured by destination, so the nav teaches the same colour
   language the cards use. */
.cats a.navlink,header>a.navlink{position:relative;text-decoration:none;
      color:var(--muted);padding:3px 2px;
      transition:color .18s var(--ease),background-color .18s var(--ease)}
.cats a.navlink::after,header>a.navlink::after{content:"";position:absolute;
      left:0;right:0;bottom:-3px;height:2px;
      background:var(--nav-c,var(--accent));transform:scaleX(0);
      transform-origin:left center;
      transition:transform .22s var(--ease)}
[dir="rtl"] .cats a.navlink::after{transform-origin:right center}
.cats a.navlink:hover,header>a.navlink:hover{color:var(--nav-c,var(--accent))}
.cats a.navlink:hover::after,header>a.navlink:hover::after,
.cats a.navlink[aria-current]::after{transform:scaleX(1)}
.cats a.navlink[aria-current]{color:var(--nav-c,var(--accent))}
/* The brand's dot blips every four seconds — and picks up the accent while
   the new-bytes pill is showing, so "there is fresh news" is said twice. */
/* NO BLIP ON THE WORDMARK. What was here scaled the `.io` by 45% every four
   seconds for ever — the last of batch J §3c's animated ornaments, left
   standing because it was §4's to remove and §4 was not reached. A wordmark
   that twitches is a wordmark asking to be looked at, which is the opposite
   of what a masthead is for.
   (The retired keyframe is NOT named here on purpose: this comment ships
    inside every page, and a check grepping the served source for that name
    would find it in the note explaining its removal. Same rule the front
    page's own comments follow about their tokens.) */
.anim body.hasnew header h1 span{color:var(--accent);
      animation-duration:1.6s}


@media (max-width:720px){
  /* 1. Tap targets. The icon stays the size it looks best at; padding does
     the work, so nothing moves and everything is reachable. */
  /* G1 §4.2: NOT CIRCLES. These were the only round shapes on the page in a
     radius-0 system. §2c allows a circle where the shape IS the meaning — a
     status dot is a status dot — and a globe inside a ring is not that: the
     ring carried nothing the glyph did not. The target stays 44x44. */
  .menubtn,.themebtn,.search .mag{min-width:44px;min-height:44px;
        width:44px;height:44px}
  .navpanel a{min-height:48px;display:flex;align-items:center;font-size:16px}


  /* 2. The search is an OVERLAY over the brand, not a row below the header.
     It is laid out at its final size from the start and revealed with a
     transform — animating width (or letting flex reflow) is exactly what
     made it nudge on every keystroke. */
  .search{position:static}
  /* Right-anchored, expanding leftward over the brand — and stopping short
     of BOTH right-hand controls, not just the menu button. The first version
     reserved 60px and put the theme toggle behind the overlay, which is a
     control a reader cannot reach while searching.
     The second reserved 104px, and that was still short. The reserve is not
     a taste value: it is the header's own right-hand geometry, added up.
     16px of header padding, the 44px menu button, the 11px header gap, the
     44px theme toggle and the 11px gap the search itself sits behind = 126.
     At 104 the open box ran to 22px PAST the magnifier it came out of and
     covered the left 11px of the theme toggle — measured at 390px: the
     closed magnifier ends at x=264, the theme toggle starts at x=275, and
     the open overlay ended at x=286. That is the "it expands rightward"
     report, and it holds at 360, 375 and 390 because every term is fixed. */
  .search.open{position:absolute;inset-inline:12px auto;
        inset-inline-end:calc(20px + 44px + 11px + 44px + 11px);
        top:50%;translate:0 -50%;
        /* The anchored edge, and the origin the reveal grows FROM. */
        transform-origin:right center;
        z-index:6;display:flex;align-items:center;gap:6px;order:0;flex:none}
  [dir="rtl"] .search.open{transform-origin:left center}
  .search.open input{flex:1 1 auto;min-width:0;width:auto;
        padding:11px 14px;
        border:1px solid var(--line);
        /* Translucent, and readable in both themes because both --panel and
           --text come from the palette rather than a literal. */
        background:color-mix(in srgb,var(--panel) 85%,transparent);
        -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
        color:var(--text);font-size:16px}
  /* The reveal grows the box leftward out of the anchored edge. A scaleX
     about `transform-origin:right center` cannot put a single frame to the
     right of the final box — the old translateX(14px) start put the leading
     frame 14px further right still, over the theme toggle, which is the
     direction this whole rule is supposed to rule out. It is also why the
     RTL variant is gone: the origin flips, so one keyframe serves both. */
  .anim .search.open{animation:bnsearch .22s var(--ease)}
  @keyframes bnsearch{from{opacity:0;transform:scaleX(.94)}
                      to{opacity:1;transform:none}}
  /* 3. A visible way out, inside the field. Esc and an outside tap do the
     same thing; this is the one a thumb can find. */
  .search.open .clear{display:inline-flex;position:absolute;
        inset-inline-end:8px;width:32px;height:32px;align-items:center;
        justify-content:center;border:0;background:transparent;
        color:var(--muted);font-size:17px;cursor:pointer}
  .search.open .clear:hover{color:var(--text)}
  .search.open .mag{display:none}

  /* 4. The hamburger MORPHS. Two bars rotate into an ✕ and the middle one
     fades — one element, pure transform, no icon swap to mistime. */
  /* the bars are defined once, in the 1199 block above */

  /* THE SECOND-OPEN BUG. Kai's phone opened the menu, closed it, opened it
     again — and "Crypto" and the language switcher were not there.
     Nothing removes them: every row in the panel, the switcher included, is
     rendered into the page by `_chrome` and stays in the DOM whether the
     panel is open or shut (measured: 11 children on open #1 and on open #5).
     What was missing was not the node, it was the PAINT.
     Each row's only route to opacity:1 was the `forwards` fill of a one-shot
     `bnitem` animation. That makes "visible" the residue of an animation
     rather than something the cascade says — and a re-open is a fresh
     animation at time zero, measured: after five open/close cycles every row
     reported `currentTime: 0` with the previous fill discarded. So any
     browser that creates the animation and does not run it — a subtree that
     just left `display:none` inside a `backdrop-filter` containing block is
     exactly where WebKit does this — leaves that row at opacity:0 for good,
     and the reader is looking at a menu with holes in it.
     The reveal is a TRANSITION out of a one-frame snapshot instead, and the
     direction matters more than the mechanism: a row's steady state — open
     or shut — is fully visible, and `.entering` is the hidden state, applied
     and removed by navjs inside a single call so it exists for exactly one
     layout. Every way this can go wrong now ends with the row on screen. No
     JS, no `.anim`: nothing is ever hidden. Class never removed, transition
     refused, motion switched off: the row is at its steady opacity, which is
     1. There is no longer any state in which a menu row's visibility is
     something the browser has to keep for us. */
  .anim .navpanel,.anim .navscrim{
        transition:opacity .24s var(--ease),transform .24s var(--ease)}
  .anim .navpanel.entering,.anim .navscrim.entering{opacity:0}
  .anim .navpanel.entering{transform:translateY(-6px)}
  .anim .navscrim{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
  /* Rows stagger in on a transition-delay set by navjs, and the delay is
     cleared on close, so the exit has no stagger at all — an exit that takes
     as long as an entrance feels broken. */
  .anim .navpanel a,.anim .navpanel .langrow,.anim .navpanel .themerow{
        transition:opacity .22s var(--ease),transform .22s var(--ease)}
  .anim .navpanel.entering a,.anim .navpanel.entering .langrow,
  .anim .navpanel.entering .themerow{
        opacity:0;transform:translateY(8px)}
  /* The current page carries an accent dot — the same "you are here" the
     desktop nav says with an underline. */
  .navpanel a[aria-current]{color:var(--accent)}
  .navpanel a[aria-current]::before{content:"";width:6px;height:6px;
        background:var(--accent);display:inline-block;
        margin-inline-end:9px}
}

/* ── Touch is not a narrow mouse (§5c) ────────────────────────────────────
   Every interactive target is at least 44x44 where touch is primary. These
   were measured by tools/responsive_sweep.py, not eyeballed: .sharebtn was
   30x30, .copyid 48x27, .perma 85x19 and the source <summary> 280x28 on
   every article page, and the tier chips came out at 40px tall inside the
   segmented control.

   An INLINE link inside a paragraph of prose is deliberately exempt — WCAG
   2.5.8 exempts it, and giving one a 44px box would double-space every
   article on the site. The sweep carries the same exemption, so the rule and
   the assertion agree. */
@media (pointer:coarse),(max-width:768px){
  .sharebtn,.copyid,.themebtn,.menubtn,.search .mag,.chip,
  .langpick>summary,#copy-btn,.btn,.perma,.fig-btn,
  .chip-tl,.chip-live,.entf,button.ent,.langmenu a,.navpanel .langrow a,
  #more-btn,.close,.back,.navlink,#pill,.w-only{
    min-inline-size:44px;min-block-size:44px}
  /* A short nav word ("Mood", "Tech") is 30px of text. The 44px floor is a
     TARGET size, not a type size, so it is reached with padding. */
  .navlink,.back{padding-inline:6px}
  /* a text field is a target too, and 39px of one inside a squeezed flex row
     is not one — let it take the space it needs */
  #share-url{flex:1 1 auto;min-inline-size:44px}
  /* inline-flex so a text control can reach the floor without the text
     growing with it */
  .perma,.copyid,.btn,#copy-btn,.sharebtn,.chip-tl,.chip-live,
  .entf,button.ent,.langmenu a,.back{
    display:inline-flex;align-items:center;justify-content:center}
  /* the sources disclosure is a real control, not prose */
  details>summary{min-block-size:44px;display:flex;align-items:center}
  #share-url{min-block-size:44px}
}

/* ── Tracking is a LATIN device (§5e) ─────────────────────────────────────
   `letter-spacing:.13em` on the kickers, labels, tabs and badges is what
   makes a mono eyebrow read as an eyebrow. Applied to a CURSIVE script it
   pulls joined letterforms apart: the Arabic strip rendered its labels as
   disconnected letters, which is not a styled label but a misspelling.
   Bengali and Devanagari have the same conjunct behaviour, and CJK gains
   nothing from Latin tracking either.

   Scoped on the EDITION's lang, not on dir: Bengali and Hindi are
   left-to-right and are damaged in exactly the same way, so keying this to
   RTL would have fixed one of the four. */
:root[lang="ar"] .kick,:root[lang="ar"] .eyebrow,:root[lang="ar"] .badge,
:root[lang="ar"] .lab,:root[lang="ar"] .chip,:root[lang="ar"] header h1,
:root[lang="bn"] .kick,:root[lang="bn"] .eyebrow,:root[lang="bn"] .badge,
:root[lang="bn"] .lab,:root[lang="bn"] .chip,
:root[lang="hi"] .kick,:root[lang="hi"] .eyebrow,:root[lang="hi"] .badge,
:root[lang="hi"] .lab,:root[lang="hi"] .chip,
:root[lang="zh"] .kick,:root[lang="zh"] .eyebrow,:root[lang="zh"] .badge,
:root[lang="zh"] .lab,:root[lang="zh"] .chip,
:root[lang="ar"] .langpick>summary,:root[lang="bn"] .langpick>summary,
:root[lang="hi"] .langpick>summary,:root[lang="zh"] .langpick>summary{
  letter-spacing:normal}
/* ── THE ITALIC OVERRIDE IS GONE, BECAUSE THE ITALIC IS (batch J3 §3) ─────
   J2 put these four editions back to roman by name, because the label
   register was Newsreader italic and these four do not download Newsreader —
   so the rule reached the platform's script face and the browser answered by
   SLANTING it, which for Arabic or Devanagari is a damaged label rather than
   a styled one, and for CJK is a convention that does not exist.

   §3 makes every edition roman, so this override has no work left: it existed
   only to undo a rule that no longer exists. Deleting it is not a relaxation
   — `check_no_synthesised_oblique` asserts the ban directly now, on the
   rendered CSS of all eight editions, which is a stronger statement than four
   `font-style:normal` declarations that happened to cover the same ground. */
/* …but a FIGURE is Latin in every edition, so the numerals keep theirs. */
:root[lang="ar"] .wnum,:root[lang="bn"] .wnum,
:root[lang="hi"] .wnum,:root[lang="zh"] .wnum{
  letter-spacing:normal;font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
/* The wordmark is a Latin brand name in all eight. Isolated so the bidi
   algorithm cannot reorder "bytenews" and ".io" around each other. */
header h1{unicode-bidi:isolate}
/* CJK must be allowed to break inside a run or a headline overflows at
   320px; Latin words in the same headline keep normal breaking. */
:root[lang="zh"] h1,:root[lang="zh"] h2,:root[lang="zh"] h3,
:root[lang="zh"] .hed{line-break:normal;word-break:normal;
  overflow-wrap:anywhere}

/* The share control. `margin-inline-start:auto` is the whole right-alignment
   rule — the meta row is a flex row, so the control takes whatever space is
   left and sits at the end of it in both directions. */
.sharebtn{position:relative;flex:0 0 auto;margin-inline-start:auto;
      display:inline-flex;align-items:center;justify-content:center;
      width:30px;height:30px;margin-block:-6px;padding:0;border:0;
      background:transparent;color:var(--muted);
      cursor:pointer;-webkit-tap-highlight-color:transparent;
      transition:color .18s var(--ease),background-color .18s var(--ease)}
/* The TOUCH target is 44px and the ink is 18px. A pseudo-element carries the
   difference, so the meta row keeps the height it had: grown to 44px the
   control would set the height of every card's meta row. 30 + 7 + 7 = 44,
   and selfcheck derives that sum out of these two rules rather than reading
   it here. What it costs is stated rather than hidden — a 44px target
   centred on an 18px row reaches 13px below the row and the gap under it is
   8, so the last 5px sit over the headline's BOX. Measured in a browser:
   that is its leading, in the 44px at the trailing edge, and every point of
   the headline's own text still hit-tests to the headline. Making the
   VISIBLE box bigger instead would put the hover tint there, which is the
   same overlap and a visible one. */
.sharebtn::before{content:"";position:absolute;inset:-7px}
/* In the flyout the control lands beside the sticky close button, and a 44px
   target on a 30px box reaches 7px past its own edge. Measured in a browser:
   that strip sat ON TOP of the ✕ — both are positioned, and this one is later
   in the document — and took the left 7px of the one control a reader needs
   to get out. The trailing margin is the reach plus two. */
#flyout .meta .sharebtn{margin-inline-end:9px}
.sharebtn:hover,.sharebtn[aria-expanded="true"]{color:var(--accent);
      background:var(--accent-bg)}
.sharebtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Two rules that size ALL svgs in their scope — `.card svg` and
   `main.article svg` at 100% width, `#flyout svg` the same with a radius and
   a margin — and this icon lives inside every one of those scopes. Both
   selectors here outrank them on class count, which is why the icon carries
   a class of its own rather than being matched as a bare `svg`. */
.sharebtn .shareico,#flyout .sharebtn .shareico{width:18px;height:18px;
      display:block;margin:0}
/* The fallback popover, for a browser with no Web Share API. Built by
   SHARE_JS when there is one to show and removed when it closes, so nothing
   here has a hidden state to leak into a page that is not running scripts.
   Positioned in the BODY, not in the card: `.card` clips its own overflow. */
.sharepop{position:absolute;z-index:40;min-width:186px;padding:6px;
      background:var(--panel);border:1px solid var(--line);
      display:flex;flex-direction:column;
      animation:bnpop .16s var(--ease)}
@keyframes bnpop{from{opacity:0;transform:translateY(-4px)}
                 to{opacity:1;transform:none}}
.sharepop .row{display:flex;align-items:center;min-height:40px;gap:9px;
      padding:0 12px;border:0;background:transparent;color:var(--text);
      font:inherit;font-size:13.5px;font-weight:400;text-align:start;
      text-decoration:none;cursor:pointer}
.sharepop .row:hover{background:var(--accent-bg);color:var(--accent)}

/* MONO, as of batch K2 §0 — the one change this batch makes to the bar, and
   it makes it on BOTH pages at once, which is the point: §2's ruling is that
   the front page and `/c/crypto/` must render the same thing, and §0's is
   that the rail is set in the figure face. A declaration that lands on one
   surface serving two pages satisfies both. Nothing else here moves: the
   size, the colours, the padding, the roll speed and the pause behaviour are
   what the category page has always had, because the desk's call is that the
   category page's bar was the correct one. */
.tickerbar{border-bottom:1px solid var(--line);background:var(--panel);
      overflow:hidden;white-space:nowrap;font-size:12.5px;
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
      font-variant-numeric:tabular-nums}
.tickerbar[hidden]{display:none}
.tk-track{display:flex;width:max-content;animation:tkroll 40s linear infinite}
.tickerbar:hover .tk-track,.tickerbar:focus-within .tk-track{
      animation-play-state:paused}
@keyframes tkroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
/* Padding on BOTH ends of a half, not just the trailing one. The track sits
   at translateX(0) until the marquee starts and a reader lands on it there,
   so a half with no leading padding puts the first symbol hard against the
   viewport edge, half of it under the phone's own screen curve — reported
   live on iOS Safari. Symmetric because the two halves have to stay
   identical: -50% is exactly one of them, and a leading pad on the first
   copy alone would make the loop jump. */
.tk-half{display:flex;align-items:center;gap:24px;padding:7px 24px}
.tk{display:inline-flex;align-items:baseline;gap:6px}
.tk b{font-weight:400;letter-spacing:.4px;color:var(--text)}
/* DECLARED RATHER THAN INHERITED (batch K2 §2). The weight budget names the
   bar's value and its change, and `.tk .c` already said 400 while `.tk .v`
   left it to the cascade — so the budget could read one of the pair and not
   the other. It computed to 400 either way; nothing about the bar moves. */
.tk .v{color:var(--muted);font-weight:400}
.tk .c{font-weight:400;color:var(--muted)}
.tk .c.up{color:var(--up)}
.tk .c.down{color:var(--down)}
.tk-note{color:var(--muted);opacity:.72;font-size:11px;letter-spacing:.3px}
/* The day's line beside the print (redesign 2026-09): dim by default,
   direction-coloured where the 24h change is, no fill, no axis — it is the
   tape, not a chart. */
.tk-spark{inline-size:44px;block-size:12px;display:inline-block;
      vertical-align:-2px;margin-inline-start:4px;overflow:visible}
.tk-spark polyline{fill:none;stroke:var(--dim);stroke-width:1.25;
      stroke-linejoin:round;stroke-linecap:round;
      vector-effect:non-scaling-stroke}
.tk-spark.up polyline{stroke:var(--up)}
.tk-spark.down polyline{stroke:var(--down)}
@media (max-width:640px){
  .tickerbar{font-size:12.5px}
  /* 16px, the same gutter the intel strip and the grid keep on a phone. */
  .tk-half{gap:16px;padding:5px 16px}
}
/* Reduced motion: the same bar, parked and swipeable, rather than nothing —
   the prices are the content, the movement is decoration. */
@media (prefers-reduced-motion:reduce){
  .tk-track{animation:none}
  .tickerbar{overflow-x:auto}
}

/* ── The widget strip — DIRECTION A (batch K2 §1) ─────────────────────────
   The desk drew it. `~/Downloads/bytenews-cards.html` -> "Direction A · the
   same seven, as an instrument strip", and this block is that drawing's
   `.wgrid .wcell .wlab .wnum .wsub .wbar .wspark .wsplit .wdot` lifted onto
   the strip the site already renders.

   WHAT CHANGED FROM BATCH K. The cell was a 104px block carrying a 14px
   label, a 26px figure, a 17px qualifier and a container-queried note. It is
   an INSTRUMENT now: a tracked uppercase label, a 19px/600 figure whose UNIT
   is a smaller `<s>` beside it, a 9.5px second line, and an ornament only
   where the drawing gives the cell one. The register is Plex Mono throughout
   — label included, which is the one thing about `.wlab` that moved.

   THE ONE DEVIATION FROM THE MOCK, AND IT IS THE DESK'S. `.wlab` is 8.8px in
   the drawing. It is 13 here (batch K3, down from K1 §2's 14): the drawing's
   size was never taken, and this batch steps toward it by a pixel rather than
   to it. Everything else about the label is the mock's — mono, .13em,
   uppercase, --dim, and an ellipsis when it runs out of cell, which at 13px
   it will more often than at 8.8.

   THE COUNT IS THE CONFIGURED COUNT, NOT THE DRAWING'S. The mock shows seven
   and `config.yaml` -> site.intel_tiles names eight; `liquidations` and
   `macro` ("next up · cpi") are configured, are not in the drawing, and stay.
   What the drawing decides is what a cell LOOKS like, not how many there are
   — so a configured cell the mock never drew simply ships with no ornament,
   which is the same rule the other four already follow.

   FULL BLEED, still. The strip is a sibling of <main>, not a child of it,
   which is what makes "full viewport width" plain layout rather than a
   `100vw` trick — `100vw` includes the scrollbar and puts a horizontal
   scrollbar on the page to prove it. */
.intel{padding:0;margin:0 0 var(--section-gap);position:relative}
/* THE COLUMN COUNT IS THE DRAWN COUNT (batch K §4, and it survives §1). It
   was a hard `repeat(8,1fr)`, and when J3 §5 cut the strip to five names the
   row kept its eight tracks: five cells filled the left 62% of a 1440px page
   and three empty columns sat beside them — visible in one glance on the live
   site and green in every gate, because nothing was counting tracks against
   cells. `--tiles` and `--tiles-half` are written onto the row by `_intel`
   from the number of tiles it actually DREW, which is not the same as the
   number configured. The mock's own `repeat(7,1fr)` is the fallback shape. */
/* ONE ROW, ONE MECHANISM, EVERY WIDTH (batch R49, Kai: "make the widgets
   full bleed and a carousel as well, like the cards carousel. also every
   widget should have the same height and width").

   It was THREE layouts: a 9-column grid above 1220, a 4-column grid that
   WRAPPED TO THREE ROWS between 621 and 1220, and a flex scroller below
   621 — so "the strip" meant a different thing at three different widths,
   and only the phone one was a carousel. It is the phone's one now, at
   every width, which is also the graphics carousel's: overflow, snap, no
   scrollbar, and the page's own edges.

   `--tiles` and `--tiles-half` are still written onto the row by `_intel`
   and are still what the count-dependent checks read; nothing lays out
   from them any more. */
.intel-row{display:flex;flex-wrap:nowrap;
      overflow-x:auto;overscroll-behavior-x:contain;
      scroll-snap-type:x proximity;scrollbar-width:none;
      border-block:1px solid var(--rule);
      /* FULL BLEED. The strip's edges are the page's, as the carousel's
         are — `env()` only, so a notch still pushes the tiles clear of
         itself and nothing else moves them in. */
      padding-inline:env(safe-area-inset-left,0px)
                     env(safe-area-inset-right,0px);
      scroll-padding-inline-start:env(safe-area-inset-left,0px)}
.intel-row::-webkit-scrollbar{display:none}
/* a scroll container is a focus target, and the UA's outline on it is drawn
   on a 0-height box */
.intel-row:focus-visible{outline:2px solid var(--acc);outline-offset:-2px}
/* `border-inline-end`, where the mock writes `border-right`. The same edge in
   all four Latin editions and the right one in Arabic — a physical `right` on
   a divider is the bug RTL_CSS exists to prevent, and lifting a rule from a
   left-to-right drawing is not a reason to ship it. */
/* ONE WIDTH, AND THE PEEK FALLS OUT OF IT (batch R49). `--w-cell` is the
   tile's width at every viewport; `flex:1 0` lets the nine GROW equally
   when they all fit and forbids them shrinking when they do not, so:

     they do not fit  ->  every tile is `--w-cell`, and the one at the
                          right edge is cut. That fraction is the whole
                          affordance — it is the only thing that says the
                          row continues (R44 §2, and its reasoning stands).
     they all fit     ->  the nine share the row exactly, still all equal,
                          and there is nothing to say: no peek is needed
                          when nothing is behind the edge.

   `--peek` is R44 §2's mechanism and it is kept, extended to every width
   this row now covers. A FIXED cell width was tried first and measured
   worse: with one width and nine tiles there are viewports where the
   tiles divide the row evenly and the next one starts exactly at or past
   the edge — 700 and 1440 both did — so a tile is HIDDEN with nothing
   cut, which is the one state R44 built the fraction to prevent. A
   fraction of the row cannot land there.

   THE LADDER SKIPS "EIGHT WHOLE". With nine tiles, a divisor that shows
   eight leaves only the ninth, and the ninth is the one that lands past
   the edge uncut (1120 at 7.25 and 1536 at 8.25, both measured). So every
   step below shows SEVEN or fewer with a cut, and above 1408 all NINE fit
   and the row does not scroll at all — no peek is needed when nothing is
   behind the edge, and `flex-grow:1` spends the spare width equally so
   the nine still match each other. */
.wcell{padding:11px 14px;border-inline-end:1px solid var(--rule2);min-width:0;
      flex:1 0 calc((100% - var(--pkn,2) * var(--pkg,12px))
                    / var(--peek,2.25));scroll-snap-align:start;
      display:block;text-decoration:none;color:inherit;
      transition:background-color .15s var(--ease)}
/* `--pkn` IS THE CONSTANT, AND IT IS NOT A CONSTANT (batch R49). To show
   `n` whole tiles and a fraction `f` of the next, the row must satisfy
   V = n*(W + gap) + f*W, so W = (V - n*gap) / (n + f). The subtracted term
   is the gaps BEFORE the cut tile — `n * gap` — and a flat 12px is only
   right when n happens to be 1.

   That is not pedantry: with the flat constant the sweep measured FOUR
   widths where the row scrolled and nothing was cut (768, 900, 959 at
   5.25 and 1215 at 6.25). The next tile began exactly in the gap past the
   edge — hidden, with no fraction to say so, which is the one state R44
   §2's peek exists to prevent. The dead window is `gap` wide out of every
   pitch, so roughly one width in thirteen; the first version of this batch
   hit it four times in a 27-width sweep and a spot check at the six named
   viewports would have missed all four.

   Each step's `n` is the whole count it shows and each boundary is where
   the cell would otherwise pass ~190px, the density the wide strip already
   shipped; the floor is ~135. `tools/strip_sweep.py` re-measures all of
   it, and its rule is the honest one: either every tile is visible, or one
   is cut — never a tile hidden with nothing cut. */
@media (min-width:440px) and (max-width:539px){.intel-row{--peek:2.75;--pkn:2}}
@media (min-width:540px) and (max-width:620px){.intel-row{--peek:3.25;--pkn:3}}
@media (min-width:621px) and (max-width:767px){.intel-row{--peek:4.25;--pkn:4}}
@media (min-width:768px) and (max-width:959px){.intel-row{--peek:5.25;--pkn:5}}
@media (min-width:960px) and (max-width:1215px){.intel-row{--peek:6.25;--pkn:6}}
@media (min-width:1216px) and (max-width:1407px){.intel-row{--peek:8.25;--pkn:8}}
/* ALL NINE FIT, so there is nothing behind the edge and no fraction to
   draw: `9` and `8` gaps is the exact-fit basis, `flex-grow` spends what is
   left equally, and the row does not scroll at all. */
@media (min-width:1408px){.intel-row{--peek:9;--pkn:8}
  /* the ONLY width class that may shrink. The basis is the exact-fit one,
     and a tenth of a pixel of rounding either way would otherwise put the
     row into a scroll with eight whole tiles — the shape the ladder above
     is built to skip. Shrinking lets the nine settle into the row exactly
     and keeps them equal to each other, which is what was asked for. */
  .wcell{flex-shrink:1}}
.wcell:last-child{border-inline-end:0}
@media (hover:hover){a.wcell:hover{background:var(--tint)}}
/* Two columns at 1220, below which the strip's gutters stop shrinking and a
   cell drops under what its figure needs. 1220 is `--wrap`'s FLOOR plus
   `main`'s 40px of padding — that is, the widest viewport at which the shell
   still fills the screen. It read "the page's own `--wrap`" until R38 §3,
   which was already 40px out (`--wrap` was 1180, `main` was 1220) and would
   have become properly wrong once `--wrap` started moving. The number is
   right and unchanged; the sentence under it was not. */
/* WHAT USED TO BE HERE (batch R49): `@media (max-width:1220px)` turned the
   strip into `repeat(--tiles-half,4)` — four columns wrapping to THREE
   rows, 418.3px of page at 700 — and the two `nth-child` rules drew the
   borders that shape implied. One scrolling row needs none of it, and a
   rule that reasons about the 4th and 5th tile is a rule about a grid. */
/* THE LABEL. Mono, uppercase, tracked, dim, and one line with an ellipsis
   when the cell is too narrow for it — which is the mock's rule and replaces
   K's "a label is words and words break". At 13px with .13em of tracking a
   long label WILL reach the ellipsis on an eight-column laptop, and that is
   the trade the size buys: a cut label costs a word, and a label wrapping to
   two lines costs every cell in the row its height.
   13/300 IS DESK-SET (batch K3), down from K2's 14/400. The weight is a real
   file — `plex-mono-300-latin.woff2`, vendored this batch — because Plex is
   static per weight and a 300 with no 300 face renders as 400. */
.wlab{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-style:normal;
      letter-spacing:.13em;text-transform:uppercase;color:var(--dim);
      font-weight:400;margin-bottom:7px;
      white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* THE FIGURE, AND THE ONE WEIGHT ABOVE 400 ON THIS PAGE (batch K2 §4). 600 is
   the mock's and it is deliberate: the strip is the one register where the
   number is the whole content of the cell, and it is the only figure register
   allowed the weight. Every live number here is tabular and zero-padded and
   its box is reserved — a countdown that reflows its own tile once a second
   is the bug that rule exists for. */
.wnum{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;font-size:24px;
      letter-spacing:-.025em;line-height:1;color:var(--ink);
      font-variant-numeric:tabular-nums lining-nums}
/* THE UNIT IS NOT THE FIGURE. `62` is the reading and `/100` is the scale it
   is read against; `0.1` is the reading and `gwei` is what it is measured in.
   Setting them at one size and one weight is what made "0.1 gwei" and
   "58.7%" nine and five characters of equal loudness. `<s>` because it is
   the shortest element with no meaning of its own — the strike is removed
   here and nothing on this site uses it for what HTML says it means. */
.wnum s{text-decoration:none;font-size:12px;font-weight:400;color:var(--dim);
      margin-inline-start:3px}
/* THE SECOND LINE. 11px, dim, mono, one line with an ellipsis. 11/300 is
   desk-set (batch K3), up from 9.5/400 — the one reading on the strip that
   was below every other rung on the page. */
.wsub{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;font-weight:400;color:var(--dim);
      margin-top:6px;white-space:nowrap;overflow:hidden;
      text-overflow:ellipsis}
/* ── The ornaments, one per cell type, and only where the drawing has one ──
   FOUR of the eight earn a micro-graphic, because four of them are a reading
   rather than a fact: a 0–100 index (`.wbar`), a year of losses (`.wspark`),
   a share of a whole (`.wsplit`) and a network that is either moving or not
   (`.wdot`). The other four are a number and get nothing to make the row look
   uniform — a decoration that carries no information is off-brand on a site
   whose pitch is that every figure is measured.

   `liquidations` LOST ITS SPLIT BAR (batch K2 §1). J §5b gave it one; the
   mock does not draw the cell at all, and the rule this batch works under is
   that a configured cell the drawing does not cover ships without an
   ornament rather than borrowing the nearest one. */
.wbar{height:3px;background:var(--rule);margin-top:8px;position:relative}
.wbar i{position:absolute;inset-inline-start:0;top:0;bottom:0;
      background:var(--acc);display:block}
.wsplit{display:flex;height:3px;margin-top:8px}
.wsplit i{display:block;height:100%}
.wspark{display:block;inline-size:100%;block-size:16px;margin-top:6px}
/* THE GAS DOT IS BACK (batch K2 §1, reversing batch J §3d). §3d took it out
   on the argument that a green dot meaning "gas is calm" is the amber mistake
   in another hue and that green is bound to DIRECTION on a figure. The mock
   draws it, and it draws it as the one cell whose reading is a STATE rather
   than a magnitude — calm, busy, spiking — which is the case §3d's rule was
   never about. It sits inside `.wsub`, beside the word, and the word is still
   what carries the reading. */
.wdot{display:inline-block;width:6px;height:6px;border-radius:50%;
      background:var(--up);margin-inline-end:5px;vertical-align:1px}
/* A CELL THAT STOPPED UPDATING MUST NOT LOOK LIKE A CALM MARKET. The mock's
   dark panel says so and this is the rule it uses; nothing on the strip sets
   it yet, and it is here so that whatever does has somewhere to land. */
.wstale .wnum,.wstale .wsub{color:var(--dim)}
/* ── THE PHONE'S STRIP IS ONE SCROLLING ROW (batch J2 §7b) ────────────────
   G §5b banned the carousel outright and the ban was wrong — but it was wrong
   about the remedy, not about the complaint. The objection was to HIDING
   cells: a strip that keeps four of its eight behind a swipe has dropped them
   for every reader who does not think to swipe. A row that SHOWS IT CONTINUES
   has not hidden anything, and the rule that replaces the ban is exactly that:

     A horizontally scrolling row is allowed WHEN IT SHOWS THAT IT CONTINUES.
     A partial cell must remain visible at the trailing edge, nothing may mask
     it, every cell must be reachable, and the count must be unchanged.

   So the quarter cell is not decoration and is not a nicety — it IS the
   affordance, and it is the whole licence for the row. There is no fade, no
   gradient and no mask over the trailing edge, because every one of those
   hides the one thing that proves there is more.

   THE ORNAMENTS STAY. An earlier version of this block carried
   `.tile .gauge,.tile .glyph{display:none}` — two of the three ornaments
   hidden on every phone while still in the DOM, which is the one shape the
   §0b inventory cannot see and is why that sweep measures computed display.
   Nothing here sets `display:none` on anything inside a cell. */
@media (max-width:620px){
  .intel-row{
      /* R44 §2'S REASONING SURVIVES ITS MECHANISM (batch R49). Two cells
         and a fraction, the fraction being the whole affordance, and all
         nine behind a swipe deliberately — a 2-column grid shows them all
         with no swipe and costs 436px of page, paid entirely by the news.
         What is gone is `--peek`: the cell is `--w-cell` wide at every
         width now, so it cannot bloat as the screen grows (the failure the
         steps were hand-computing away) and the extra width buys another
         tile by itself. `_STRIP_PHONE` is still the measurement.

         `mandatory` where a phone shows two tiles and a bit: the snap is
         the gesture there. Above 620 the base rule's `proximity` lets a
         wide row rest anywhere, which is what the cards carousel does. */
      scroll-snap-type:x mandatory;
      /* the gap steps down with everything else on a phone, and `--pkg`
         moves with it so the basis keeps subtracting the right thing */
      --pkg:8px}
  .wcell{border-inline-end:1px solid var(--rule2)}
  .wcell:last-child{border-inline-end:0}
}
/* THE TWO PEEK STEPS ARE GONE (batch R49). They existed to stop the cell
   bloating as the screen grew while the divisor stayed flat — 264.9px at
   620 against 156.8 at 1366. A fixed `--w-cell` cannot bloat: the width
   does not depend on the viewport, so the extra width buys another tile
   by itself, which is what the steps were hand-computing. */
@media (max-width:720px){
  /* A 360px screen wants its gutters back; the cell's own padding does not
     move, because the strip is not a different widget on a phone. */
  :root{--edge-pull:7px}
}

/* ── The price rail (§2) and the live band (§3) ────────────────────────────
   Two full-bleed surfaces added around the widget strip, which stays exactly
   where it was:

       masthead + nav
       market rail          §2
       instrument strip     already built, unmoved
       live band            §3
       front-page grid

   A RAIL MAY SCROLL SIDEWAYS AND A STRIP MAY NOT, and the distinction is not
   a preference. A rail is a SEQUENCE — six instruments in a fixed order, and
   a reader who sees BTC, ETH and SOL has read the beginning of it and knows
   there is a rest. A strip is a SET — eight unrelated readings, and one
   hidden behind a swipe is one that has been dropped for every reader who
   does not think to swipe. §5b forbids the carousel for the strip and this
   comment exists so nobody "fixes" the rail into it, or the strip into this. */

/* ── the rail: RETIRED (batch K2 §2) ──────────────────────────────────────
   The front page rendered a section carrying the `rail` class — a server-
   rendered lane of 24 instruments at 17px with a coin mark against each,
   rolling at 60s — and `/c/crypto/` rendered the `tickerbar` element, the
   client-side marquee at 12.5px with no marks, rolling at about 55px a
   second. (Neither element is SPELLED as markup here on purpose: this
   comment ships inside every page of the tree, and more than one check in
   this repo greps the served body for the first occurrence of a tag. The
   trap has fired twice already — see `check_intel`.) Two
   surfaces, two stylesheets, two speeds, both called the rail. The desk saw
   them side by side and ruled that the category page's is the correct one;
   the front page now emits exactly that markup, from the same `_ticker()`
   call, so the two pages are byte-identical rather than merely similar.

   Everything below this comment used to be `.rail .rail-lane .rl-track
   .rl-half .rl .rl .mk .rl b .rl .v .rl .c .rl.stale .rl .stalemark` and
   `@keyframes rlroll`. It is gone with `_rail`, `_rail_cell` and `RAIL_JS`.
   `tools/ui_inventory.py` -> RETIRED carries the record, `tools/rail_probe.py`
   drove a control that no longer exists, and `band.rail`/`band.rail_lane`
   stay in `newsagent/band.py` as the data helpers that build
   `data/ticker.json`, which the bar that replaced this one reads.

   WHAT IT COST, STATED RATHER THAN BURIED: the front page's prices were REAL
   MARKUP ON LOAD and are now fetched by script. A reader with scripting
   blocked had a rail and has a bar that never unhides. That is a real loss
   and it is the desk's call, not an oversight. */

/* ── the band ──────────────────────────────────────────────
   THE BAND HAS NO HEIGHT BUDGET (batch K §0). It had one from I1 §2 until
   this batch, and the budget is retired — with `--band-above`,
   `--band-gap`, `--card-floor`, the donor arithmetic that fed them, and the
   two phone overrides that paid for them.

   WHAT BOUGHT THE CAP WAS REAL AND WHAT IT GREW INTO WAS NOT. I1 §2
   measured a first `<article>` showing 46 of its 388 pixels at 1366x768, and
   capping the band fixed that. What followed was not asked for: a `max-height`
   built from three measured constants, the band head deleted on phones (J4),
   the lead figure deleted on phones (J4), the widget strip cut from eight
   cells to five (J3 §5), and §8's threshold line left with nowhere to live.
   Five surfaces removed from the page to keep one arithmetic true. The desk:
   "if you put Last update: 5 minutes ago, maybe the news bit go down a little
   but so what? when did i restrict that this cant happen." Nobody did. The
   rule was invented here.

   THE CONSTRAINT IS A REPORT NOW, and it makes a weaker promise on purpose:
   the band is ENTERED above the fold at every width — the head and at least
   one whole row are on screen before a reader scrolls — and the lead card's
   top and its headline's bottom are MEASURED and PRINTED at five viewports
   rather than forbidden from moving. A number that moves is then visible in
   a diff instead of being illegal. `selfcheck.check_band_fold` is that
   report; `tools/fold_probe.py` is the instrument that feeds it.

   `min-height:0` STAYS, and it is the only part of I1 §2 that does. A flex
   item's default `min-height:auto` is its content, so a `.band-list` without
   it refuses to shrink and any cap above it is a no-op — which makes it the
   best negative control in this file, and it is still driven as one. The list
   may still give way if something above it ever needs the room. It is simply
   no longer the mechanism that decides what ships. */
.band{border-bottom:1px solid var(--rule);margin:0;
      display:flex;flex-direction:column}
.band-head,.band-down,.band-quiet{flex:0 0 auto}
.band-head{display:flex;align-items:center;gap:12px;
      padding:9px max(20px,env(safe-area-inset-right),
                      calc((100% - var(--wrap)) / 2))
              9px max(20px,env(safe-area-inset-left),
                      calc((100% - var(--wrap)) / 2));
      border-bottom:1px solid var(--rule2)}
/* NOT AMBER (batch J §3d, correcting G §2b, which is mine and was wrong).
   G gave `--live` "the live band, tick indicators, watching / not-watching
   state", which put HEALTHY and DEGRADED in the same colour: the LIVE marker
   and the WATCHERS UNKNOWN notice were both amber, on a site whose standing
   rule is that a quiet feed and a broken feed must look different. It also
   put four accents on one screen — oxblood, amber, green, red.

   Corrected: a healthy state carries NO accent. The word LIVE is furniture
   and sits in --dim; the age beside it is the signal, and the fact that it
   MOVES is what says the wire is alive. A page where nothing is wrong should
   have nothing shouting on it, and amber used that sparingly becomes
   legible — amber means look here. */
.band-head h2{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:12.5px;font-weight:400;
      letter-spacing:.13em;text-transform:uppercase;color:var(--dim);
      display:inline-flex;align-items:center;gap:7px}
/* The wire's last two days as 48 bars (redesign 2026-09). Dim, the current
   hour in the accent; drawn from the rows, bumped by the poll. */
.band-hist{inline-size:96px;block-size:12px;color:var(--dim);
      margin-inline-start:14px;align-self:center;flex:0 0 auto}
.band-hist rect{fill:currentColor}
.band-hist .base{stroke:var(--rule);stroke-width:.7;vector-effect:non-scaling-stroke}
.band-hist .tick{fill:var(--dim);opacity:.7}
.band-hist rect:last-child{fill:var(--acc)}
/* THE THRESHOLD LINE IS RETIRED (batch K §3), not rehomed. `.thr` carried
   "transfers ≥ $25M · liquidations ≥ $1M · moves ≥ 2%" — §8's "the page
   STATES the threshold it used". The desk's call is that the front page does
   not need it, and J4's open item asking where it should live on a phone is
   retired with it. The bars are still one function (`band.thresholds`), still
   the only reader of `band.thresholds` in config, and still substituted into
   BAND_JS, so the filter and the client cannot disagree about what is on the
   page — what is gone is the sentence, not the single source.
   `tools/ui_inventory.py` → RETIRED carries the record.
   THE LAST-UPDATE LINE IS WHAT THE HEAD SAYS INSTEAD (§3): "last update 5m
   ago", one translated template with the counting age substituted into it,
   so the word order is the edition's and not English's. */
.band-head .upd{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:13px;color:var(--dim);
      margin-inline-start:auto;text-align:end;min-width:0;
      display:inline-flex;align-items:baseline;gap:6px;white-space:nowrap}
/* ── THE BAND IS A TAPE (batch L §8) ──────────────────────────────────────
   Five fixed rows became twenty-four in the DOM and four on the screen. The
   fold arithmetic batch K §0 retired does not come back with it and cannot:
   four rows in a box of a fixed, small height is a constant, and the twenty
   behind them cost nothing above it.

   `--bd-row` is the row's own height and `--bd-shown` the count PLUS the
   peek. §8's rule is that a scrolling region must show there is more, and
   half of the fifth row sitting on the bottom edge is the cheapest possible
   way to say so — the same rule §7b set for the strip horizontally, turned
   ninety degrees. `[data-more]` fades that edge with it, and BAND_JS takes
   the fade off at the end of the list, because a permanent one is a lie
   about there being more.

   MEASURED, NOT ASSERTED. `check_band_tape` counts the rows whose boxes are
   fully inside the scroll viewport at 1440 and at 390 — a band row wraps to
   two lines inside 390px often enough that a number derived on paper would
   be wrong there. */
.band-list{list-style:none;padding:0;margin:0;
      --bd-row:42px;--bd-shown:4.5;
      max-height:calc(var(--bd-row) * var(--bd-shown));
      /* Scrolling INSIDE the band is itself a pause (see BAND_JS), which is
         the gesture a reader reaches for before they find a button. */
      overflow-y:auto;overscroll-behavior-y:contain;
      /* `overflow-anchor:none`, and it is REQUIRED rather than tidy. Chrome
         anchors a scrolled box to the content in it, so a row inserted at
         the top pushes `scrollTop` down to keep what you were looking at
         still — which for a reader AT THE TOP means the new row is scrolled
         out of sight the instant it arrives. §8 asks for the opposite: at
         the top, a new row pushes in and STAYS VISIBLE. Off. The
         scrolled-down case does not need it, because a held row is not
         inserted at all. */
      overflow-anchor:none;
      /* The region scrolls from the keyboard, and does so with no script:
         the markup carries `tabindex` and `role="feed"`, and Firefox needs
         the tabindex before a scrollable box will take focus at all. */
      scroll-behavior:smooth;
      /* THE LIST IS STILL WHAT WOULD GIVE WAY, and `min-height:0` is still
         the load-bearing half — a flex item's default `min-height:auto` is
         its content, so without it the list refuses to shrink and any cap
         above it does nothing at all except overflow. There is no cap above
         it as of batch K §0 and there is no second ceiling here either: the
         band holds five rows (§2) and draws all five. This declaration is
         kept because it is the file's best negative control and because the
         day something above the list does need the room, this is what pays
         — not because anything is currently shrinking. */
      flex:1 1 auto;min-height:0}
@media (prefers-reduced-motion:reduce){.band-list{scroll-behavior:auto}}
/* AN ARRIVAL MOVES (redesign 2026-09). A row the poll inserts after the page
   is up slides in from the head and its ground cools from the tint to the
   paper over a second; the rows the render wrote get nothing, because they
   did not arrive — the list is marked `live` only once the poll is running
   and `@starting-style` only fires for a node inserted after that. Off with
   the rest of the motion under `prefers-reduced-motion` (no `.anim`). */
.anim #band-list.live>.bd{transition:opacity .45s var(--ease),
      transform .45s var(--ease),background-color 1.2s var(--ease)}
@starting-style{
  .anim #band-list.live>.bd{opacity:0;transform:translateY(-8px);
      background-color:color-mix(in srgb,var(--live) 18%,var(--pap))}
}
.band-list:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.band-list[data-more="1"]{
      -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 22px),
                                         transparent);
      mask-image:linear-gradient(to bottom,#000 calc(100% - 22px),transparent)}
/* ── "N NEW", AND WHY IT EXISTS (batch L §8) ─────────────────────────────
   A feed that yanks the page while somebody is reading row eleven is worse
   than a feed that does not update. At the top, a new row pushes in and
   stays visible; scrolled down, the reader's position is PRESERVED to the
   pixel and this appears instead. Pressing it returns to the top and
   flushes what was held. A real <button>: keyboard-reachable, announced,
   and it says how many. */
.band-wrap{position:relative}
.band-new{position:absolute;inset-block-start:6px;inset-inline-start:50%;
      transform:translateX(-50%);z-index:3;
      font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:11.5px;letter-spacing:.09em;
      text-transform:uppercase;padding:5px 12px;
      border:1px solid var(--rule);background:var(--tint);color:var(--ink);
      cursor:pointer}
.band-new[hidden]{display:none}
.band-new:hover{border-color:var(--accent);color:var(--accent)}
/* §9: THE TWO STATES ARE VISIBLY DIFFERENT. A row that goes somewhere is
   underlined and takes the accent on hover; a row that does not is plain text
   with no affordance at all, so a reader learns which rows go somewhere by
   the second kind never pretending to.

   KEYED ON `data-dest` AS OF BATCH L1 §2, not on `data-story`. A story is no
   longer the only destination a row can have — an on-chain row goes to its
   row on the ledger — and `data-story` still means exactly what it meant, so
   a row with a story carries both. What must stay true is that this selector
   matches EVERY unlinked row and no linked one, which is what
   `check_band_reads` asserts against the rendered band. */
.bd:not([data-dest]) .tx{cursor:default}
/* ── A BAND ROW IS 17px AND 42px TALL (batch K §1) ────────────────────────
   It was 13.5px on a page whose lead card ran a 126px figure — two type
   systems on one screen, one shouting and one whispering. 17 is the ladder's
   INLINE rung, which is also the rung a figure inside this row takes, so the
   sentence and the number it carries are one size and read as one line.

   42px OF ROW, and the padding is derived rather than chosen: the row is a
   1px rule plus its own line box plus two paddings, and the line box is
   17 x 1.5 = 25.5 inherited from `body`. (42 - 1 - 25.5) / 2 = 7.75. The
   phone's is 46 by the same arithmetic — (46 - 1 - 25.5) / 2 = 9.75 — and
   the FONT does not change with the width: a phone is not a shrunken
   desktop, only its column count and its sentence length differ. Both are
   affordable only because §0 took the cap off. */
.bd{display:grid;grid-template-columns:auto 1fr auto;position:relative;
      align-items:baseline;gap:0 10px;
      padding:7.75px max(20px,env(safe-area-inset-right),
                      calc((100% - var(--wrap)) / 2))
              7.75px max(20px,env(safe-area-inset-left),
                      calc((100% - var(--wrap)) / 2));
      border-bottom:1px solid var(--rule2);font-size:17px;
      /* §2: a band row is a list of readings, not a list of claims. */
      font-weight:400}
.bd:last-child{border-bottom:0}
.bd time{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--dim);
      font-variant-numeric:tabular-nums lining-nums;white-space:nowrap}
/* THE FIGURE INSIDE THE SENTENCE IS THE INLINE RUNG, STATED (batch K §1 and
   §7). It is 17 because the row is 17 and a figure set apart from the words
   around it is the 126px-over-13.5px mistake in miniature. Declared rather
   than inherited so `check_type_ladder` has a rule to read: a number-bearing
   selector must name its rung. */
.bd .fig{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:16px;font-weight:400;
      color:var(--ink);font-variant-numeric:tabular-nums lining-nums;
      white-space:nowrap}
/* THE SPEED CHIP IS GONE (batch J §2d). It was meant to let a reader tell a
   five-second-old liquidation from a forty-minute-old story without reading
   the timestamp — but every row on the live band said SECONDS, so it
   distinguished nothing, and what it actually named was which stream
   produced the row. The time column answers the question it was built for,
   and answers it exactly. */
.bd .tx{min-width:0;overflow-wrap:anywhere;color:var(--ink)}
/* THE REFERENCE CLAUSE (batch L §2). "— the largest hour since Tuesday" is
   the half of the row that makes it news rather than a reading, and it is
   set DOWN rather than up: upright, dimmed, the same weight as the sentence
   it qualifies. `font-style:normal` because `<em>` is italic by default and
   the mono figures beside it have no italic to match. */
.bd .tx em{font-style:normal;color:var(--dim)}
.bd .tx a{color:inherit;text-decoration:none;
      border-bottom:1px solid var(--rule)}
.bd .tx a:hover{color:var(--acc);border-bottom-color:var(--acc)}
/* THE WHOLE ROW IS THE TARGET (batch J §2d). One anchor stretched over the
   row, not a second link and not an onclick: the a11y tree still holds
   exactly one link per row, keyboard focus still lands on the text, and a
   reader with no CSS still gets an ordinary inline link. */
.bd .tx a::after{content:"";position:absolute;inset:0}
.bd .sr{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:13px;color:var(--dim);
      white-space:nowrap;justify-self:end;
      /* above the row overlay, so the attribution stays its own link */
      position:relative;z-index:1}
.bd .sr a{color:var(--dim)}
/* `display:inline-block`, and it is not cosmetic. A <span> is `display:inline`
   by default, and an inline box IGNORES width and height — so the <svg>
   inside fell back to its intrinsic 300x150 and rendered a Bitcoin mark the
   full width of a phone, between the notice and the first row. It works in
   `.rl` only because that is a flex container, which blockifies its children.
   Caught by looking at the 390px screenshot; every gate was green over it,
   because a 300px mark is present, correctly coloured and correctly
   referenced — it is just the wrong size, which nothing was measuring. */
.bd .mk{display:inline-block;width:15px;height:15px;color:var(--ink);
      vertical-align:-2px;margin-inline-end:6px}
.bd .mk svg{width:100%;height:100%;display:block;fill:currentColor}
/* The mark never outgrows the figure beside it: 15px against a 17px line is
   under the cap height of the mono face, which is the §5 rule and applies
   wherever a mark is a label. It tracked the row when the row was 13.5 and
   it tracks it now that batch K §1 made the row 17. */
/* A QUIET BAND AND A BROKEN BAND MUST LOOK DIFFERENT. This is the broken
   one: a named watcher, and since when. On 2026-08-17 the Bitcoin socket was
   dead for hours while Binance liquidations kept arriving, and a tape with no
   row like this looked perfectly alive. */
.band-down{display:block;padding:9px max(20px,calc((100% - var(--wrap)) / 2));
      border-bottom:1px solid var(--rule2);
      background:var(--tint);font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:13px;
      color:var(--live-ink);border-inline-start:3px solid var(--live)}
.band-down b{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-weight:400;letter-spacing:.06em}
.band-quiet{padding:14px max(20px,calc((100% - var(--wrap)) / 2));
      font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:13px;color:var(--dim)}
@media (max-width:620px){
  /* The source column is the least load-bearing end of a band row and it is
     recoverable — every row's text links to the thing itself. Hidden, never
     deleted: the §0b inventory runs at 390 to prove it is still in the DOM.
     UNCHANGED BY BATCH K: §1 makes the row's own type the same size on a
     phone as on a laptop, and §7 puts the source on the right of a row; it
     does not ask for a third column back inside 360px, and reversing J's
     call here is not this batch's. */
  .bd{grid-template-columns:auto 1fr;
      /* 46px of row, derived: (46 - 1 rule - 17 x 1.5 line) / 2 = 9.75.
         THE FONT DOES NOT CHANGE — the `font-size:13px` that used to sit
         here is gone (batch K §1). */
      padding-block:9.75px;
      /* ── AND THE PHONE ROW IS A FIXED TWO LINES (batch L §8) ─────────
         Derived, not chosen: 2 x 25.5 line + 2 x 9.75 padding + 1 rule =
         71.5. Every row on a phone is exactly this tall — a one-line row is
         padded out to it and a longer one is clamped to two lines below —
         so `--bd-shown` is a row COUNT rather than an estimate.

         WHY IT HAD TO BE FIXED. Measured at 390x844 with the batch's own
         rows, three were visible; measured again after §4 made the
         sentences longer, TWO were. The count was tracking the length of
         whatever the market had just done, which is not a layout. A
         one-line row set to 46px would have been deterministic too and it
         is the desk's drawing — `white-space:nowrap;text-overflow:ellipsis`
         at every width — but it clips a headline on the surface that has
         the least room for one, and this repo's standing rule is that a
         line written to be clipped is not a line. Paying 25.5px of height
         to keep the words is the trade. */
      min-height:71.5px;align-content:center}
  .bd .sr{display:none}
  /* THREE ROWS AND HALF OF A FOURTH (batch L §8). 46px is the phone row
     derived seven lines above; the half-row is the cue that the tape
     continues, and it is what absorbs a sentence that wraps.

     MEASURED AT 390x844 ON A REAL RENDER: 3 rows fully inside the box, a
     4th partly in view, 24 in the DOM, and the page does not scroll
     sideways. It is 3 rather than 4 because the phone row is taller and the
     box is shorter, both deliberately.

     AND THE COUNT HAS A FLOOR. A band row wraps here — the source column is
     gone but the sentence is not shortened — so a row is 46px on one line
     and 71.5px on two, and three long rows in a row would show TWO. The
     clamp below is what stops it going lower than that: no row may take
     three lines, whatever arrives in it. A clamped row is the one place on
     this site where text is cut, and it is cut with an ellipsis on a row
     whose full text is one tap away on the story it links to — which is the
     trade the desk's own drawing makes (`.x{white-space:nowrap;overflow:
     hidden;text-overflow:ellipsis}`, at every width) and this takes half of. */
  .band-list{--bd-row:71.5px;--bd-shown:3.5}
  .bd .tx{display:-webkit-box;-webkit-box-orient:vertical;
      -webkit-line-clamp:2;line-clamp:2;overflow:hidden}
  /* WHAT USED TO BE HERE, AND WHY IT IS NOT (batch K §0).
     `.band-list{max-height:210px}` — a second cap under `.band`'s, and the
     one that actually bound at 390x844.
     `.band-head{display:none}` — J4's first donor. The head is back at
     every width; §3 is the whole reason.
     `.band{--band-above:224px;--band-gap:21px;--card-floor:242px;
      max-height:clamp(...)}` — the phone's re-derived budget.
     Its other half, `.card.lead>.top{display:none}` in THEME_CSS, is gone
     too, so a phone gets the lead's picture back. All five were bought by
     one arithmetic and the arithmetic is retired: the band is entered above
     the fold at every width, the lead card's position is measured and
     printed rather than forbidden to move, and `check_band_fold` is that
     report. */
}
@media (prefers-reduced-motion:reduce){
  .bd{animation:none !important}
}

/* ── AN AGE THAT COUNTS, WHERE A DOT USED TO PULSE (batch J §3c) ──────────
   A pulsing dot pulses whether or not the feed is alive. It is decoration
   wearing the costume of a status, and it is the most overused element in a
   machine-built dashboard after the gradient card this design already threw
   out. Every status dot on this site is now a number:

       3s   since last row                      healthy, quiet or busy
       41m  Bitcoin — not watching since 17:32  dark, and unmistakable

   The number only moves when the data moves. If a feed dies the number
   climbs and a reader watches it climb, which is a thing a CSS animation
   cannot fake — that is the whole point.

   IT DELETES A CLASS OF BUG. There is no separate "broken" state to remember
   to render, so a quiet feed and a broken feed cannot look the same. That is
   the 2026-08-17 rule expressed as a graphic instead of as a paragraph.

   §3d: A HEALTHY STATE CARRIES NO ACCENT. The figure sits in --ink and its
   caption in --dim, like every other figure on the site. Amber is reserved
   for the degraded state alone, so amber can mean LOOK HERE. */
.age{display:inline-flex;align-items:baseline;gap:6px;min-width:0}
.age b{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:400;font-size:12px;
      font-variant-numeric:tabular-nums lining-nums;color:var(--ink);
      /* A RESERVED BOX. The value goes 3s → 41m → 2h and a box that resizes
         once a second is the bug the strip's countdown rule exists for. */
      min-width:4ch;text-align:end;font-feature-settings:"tnum"}
.age .what{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:12px;color:var(--dim);
      white-space:nowrap}
/* A CAPTION IS DROPPED, NEVER CUT. Measured at 390: "5h since last row"
   next to LIVE and the threshold line leaves the caption 40px, and an
   ellipsis on "since last r…" is the truncation §2 bans. Same rule a dropped
   table column follows — still in the DOM, still read aloud, just not drawn.
   The figure never goes: it is the reading. */
@media (max-width:520px){.band-head .age .what{
      position:absolute;width:1px;height:1px;overflow:hidden;
      clip-path:inset(50%);white-space:nowrap}}
/* THE DEGRADED STATE, AND ONLY THE DEGRADED STATE, IS AMBER. --live-ink is
   the 4.52:1 value; --live is the 3.33:1 one and may only carry a non-text
   indicator, which after §3c there are none of. */
.age.dark b,.age.dark .what{color:var(--live-ink)}
td .age{gap:5px}
td .age b{font-size:12px}

/* THE MARKET PANEL — 2026-09, second pass (Kai: "smooth, soothing for the
   eye … more innovative graphics with the data"). Every published instrument
   is a raised tile carrying its smoothed 24-hour line over a gradient wash;
   under it the BYTE 500 as a MARKET MAP — every tracked coin with a tape,
   sized by its 24-hour volume and coloured by its 24-hour move. The bar it
   replaces stays in the document for the script that patches the panel. */
/* THE MAP. A squarified treemap: area is the coin's 24-hour volume on the
   BYTE 500's own record, the wash is its 24-hour move off the tape, capped
   at 5% so the map reads relative strength rather than one outlier. */
.mm{padding:2px max(20px,env(safe-area-inset-right),calc((100% - var(--wrap)) / 2))
            10px max(20px,env(safe-area-inset-left),calc((100% - var(--wrap)) / 2))}
.mm svg{display:block;width:100%;height:auto;border-radius:var(--r2);overflow:hidden}
.mm svg.tall{display:none}
@media (max-width:620px){.mm svg.wide{display:none}.mm svg.tall{display:block}}
.mm-t text.s{font-size:9px}
/* THE GRAPHICS BETWEEN THE CARDS: full-width grid items, a head line and a
   drawing. Charts are path-only SVGs stretched to a fixed height with their
   labels in HTML, so the type stays crisp at every width. */
.grid>.ig{grid-column:1/-1;padding:8px 2px 2px}
.ig-head{display:flex;align-items:baseline;gap:10px;padding:0 2px 8px}
.ig-head a,.ig-head b{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-weight:800;font-size:13px;letter-spacing:.01em;
      color:var(--ink);text-decoration:none}
.ig-head a:hover{color:var(--acc)}
.ig-head span{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;letter-spacing:.14em;
      text-transform:uppercase;color:var(--dim)}
.ig-head span s{text-decoration:none;font-size:9px}
.ig-chart{display:block;width:100%;height:120px;overflow:visible;
      background:var(--surf);border:1px solid var(--rule2);border-radius:var(--r2);
      box-shadow:var(--shadow);padding:8px;box-sizing:border-box}
.ig-x{display:flex;justify-content:space-between;padding:6px 10px 0;
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;color:var(--dim);letter-spacing:.06em}
.ig-x span{flex:1 1 0;text-align:center}
.ig-x.two span:first-child{text-align:start}
.ig-x.two span:last-child{text-align:end}
.ig.lg rect{fill:var(--live)}
.ig.lg rect.stablecoin_mint{fill:var(--up)}
.ig.lg rect.stablecoin_burn{fill:var(--down)}
.ig.lg rect.liquidation_cascade{fill:var(--acc)}
.ig.lg rect.token_transfer{fill:var(--dim)}
.ig.hk rect{fill:var(--rule)}
.ig.hk rect.hi{fill:var(--down)}
.ig.md .mid{stroke:var(--rule);stroke-width:1;stroke-dasharray:4 4;vector-effect:non-scaling-stroke}
.ig.md .a{fill:color-mix(in srgb,var(--acc) 16%,transparent)}
.ig.md .l{fill:none;stroke:var(--acc);stroke-width:1.8;vector-effect:non-scaling-stroke;stroke-linejoin:round}
.ig-plot{position:relative;flex:1 1 auto;min-height:120px}
.ig-plot .ig-chart,.gc .ig-plot .ig-chart{position:absolute;inset:0;height:100%;min-height:0;flex:none}
.ig-plot .pt{position:absolute;width:11px;height:11px;margin:-5.5px 0 0 -5.5px;border-radius:50%;
      background:var(--surf);border:1.5px solid var(--acc);box-sizing:border-box;cursor:pointer;
      transition:transform .2s var(--ease),background-color .2s var(--ease)}
.ig-plot .pt:hover{transform:scale(1.35);background:var(--acc)}
.anim.ready .ig-plot .pt{animation:bnpop .5s var(--ease) both;animation-delay:calc(var(--i,0)*30ms)}
.anim.ready .ig.md .l{stroke-dasharray:1;stroke-dashoffset:1;animation:bndraw 1.6s var(--ease) forwards}
.mv{display:grid;grid-template-columns:1fr 1fr;gap:8px 18px;background:var(--surf);
      border:1px solid var(--rule2);border-radius:var(--r2);box-shadow:var(--shadow);padding:12px 14px}
.mv-col{display:grid;gap:6px;min-width:0}
.mv-row{display:grid;grid-template-columns:52px 1fr 60px;align-items:center;gap:8px;
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;font-variant-numeric:tabular-nums lining-nums}
.mv-row b{font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis}
.mv-row i{display:block;height:8px;border-radius:999px;background:var(--rule2);position:relative;overflow:hidden}
.mv-row i::after{content:"";position:absolute;inset:0 auto 0 0;width:var(--w,0%);border-radius:999px;background:var(--up)}
.mv-row.down i::after{background:var(--down)}
.mv-row span{text-align:end;color:var(--up)}
.mv-row.down span{color:var(--down)}
.anim.ready .mv-row i::after{animation:bngrow .9s var(--ease) both}
@keyframes bngrow{from{width:0}}
@media (max-width:620px){.mv{grid-template-columns:1fr;gap:6px}.mv-row{grid-template-columns:44px 1fr 56px}}
/* NEWS TODAY: one box, one scrollable list */
.today{padding:0 max(20px,env(safe-area-inset-right),calc((100% - var(--wrap)) / 2))
            14px max(20px,env(safe-area-inset-left),calc((100% - var(--wrap)) / 2))}
.today-box{background:var(--surf);border:1px solid var(--rule2);border-radius:var(--r);
      box-shadow:var(--shadow);padding:12px 16px 4px}
.today-list{list-style:none;margin:0;padding:0;max-height:220px;overflow-y:auto;overscroll-behavior:contain}
/* THE SAME SIZE AS A BAND ROW (batch R51, Kai: "the font of titles today on
   the wire should be same size as the titles font in live band"). It was
   14px/21 against the row's 16px/24 — same family, one notch down for no
   stated reason. `--bd-fs` is the variable R48 made for exactly this: the
   band's row, the slab's live line and this now read one declaration, and
   `1.5` is the ratio at both of its steps. */
.today-list li{display:flex;gap:10px;align-items:center;padding:9px 0;border-top:1px solid var(--rule2);
      font-size:var(--bd-fs,16px);line-height:1.5;min-width:0}
.today-list time{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;color:var(--dim);flex:0 0 auto;
      font-variant-numeric:tabular-nums lining-nums}
.today-list .badge{flex:0 0 auto}
.today-list a{color:var(--ink);text-decoration:none;min-width:0;overflow-wrap:anywhere}
.today-list li{align-items:flex-start}
.today-list time,.today-list .badge{margin-top:2px}
.today-list a:hover{color:var(--acc)}
/* THE GRAPHICS CAROUSEL: four raised cards in a snapping row */
.gcar{padding:0 0 6px}
/* THE CAROUSEL RUNS TO THE PAGE'S EDGE (batch R47 §4b). It was padded to
   the CONTENT edge - `calc((100% - var(--wrap)) / 2)`, 160px in at 1920
   (measured) - which pinned the first card where the grid starts and made a
   scrolling strip look like a centred block. A carousel wants to run off the
   edge: that is how the cards say they continue.

   THE MASTHEAD'S EDGE, and it is the masthead's own number rather than a new
   one (`header{padding:10px 24px}`). The page now has two deliberate edges -
   chrome at 24 and content at the wrap - and the carousel joins the chrome
   because it is a rail, not a column of prose. The strip and the grid are
   untouched and keep the content edge.

   `scroll-padding-inline-start` moves with it or the snap lands the first
   card off its own start. Logical properties on both, so /ar/ mirrors both
   ends; `env(safe-area-inset-*)` stays ahead of the 24 for a notch.

   AND THE CARDS THEMSELVES REACH IT NOW (batch R48, Kai: "card carousel
   byte500 and so should be full bleed"). R47 §4b moved the ROW to the
   chrome edge but kept 24px of padding inside it, so the thing a reader
   actually sees — the cards — still started 24px in and the carousel still
   read as an inset block. The inline padding is gone: the first card's
   left edge is the page's left edge. The 4px/18px block padding stays (it
   is the cards' shadow room, not an edge) and the safe-area inset stays as
   the floor, so a notch still pushes the cards clear of itself. */
.gcar-row{display:flex;gap:14px;overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none;
      scroll-snap-type:x proximity;
      padding:4px env(safe-area-inset-right,0px)
              18px env(safe-area-inset-left,0px);
      scroll-padding-inline-start:env(safe-area-inset-left,0px)}
.gcar-row::-webkit-scrollbar{display:none}
/* ONE HEIGHT FOR EVERY CARD, and it is the map's: the BYTE 500 box is
   1000×280 at the card's width, so the card is that plus its head and
   padding, and every other card fills it — rows scroll inside, nothing
   stretches the row (the desk: "keeping the same size it was"). */
.gc{flex:0 0 var(--gc-w);block-size:calc((var(--gc-w) - 34px) * .395 + 62px);box-sizing:border-box;
      overflow:hidden;scroll-snap-align:start;background:var(--surf);border:1px solid var(--rule2);
      border-radius:var(--r);box-shadow:var(--shadow);padding:14px 16px 12px;display:flex;flex-direction:column;
      min-width:0;transition:transform .5s var(--ease),box-shadow .5s var(--ease)}
@media (hover:hover){.gc:hover{transform:translateY(-2px);box-shadow:var(--shadow-up)}}
.gc .ig-chart,.gc .mv{box-shadow:none;border:0;background:transparent;padding:0}
/* every chart FILLS its card to the height the tallest card sets — a
   stretched SVG under a fixed head, the labels pinned at the foot */
.gc .ig-chart{flex:1 1 auto;height:auto;min-height:120px}
.gc .mm svg{border-radius:8px}
.gc .mv{flex:1 1 auto;padding-top:4px;align-content:space-evenly;gap:6px 18px}
.gc .mv-col{align-content:space-evenly}
.gc .ig-x{margin-top:auto}
.gc [data-tip]{cursor:pointer}
.mm-t rect,.ig.lg rect,.ig.md circle{transition:filter .25s var(--ease),opacity .25s var(--ease)}
.ig.lg rect:hover,.ig.md circle:hover{filter:brightness(1.15)}
.bn-tip{position:fixed;z-index:20;transform:translate(12px,14px);background:var(--ink);color:var(--pap);
      font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.35;padding:6px 9px;border-radius:8px;
      pointer-events:none;opacity:0;transition:opacity .15s var(--ease);max-width:260px;white-space:pre-line}
.bn-tip.on{opacity:1}
@media (max-width:720px){
  .today{padding:0 10px 10px}
  /* the padding steps down on a phone; the SIZE does not, because it is
     the band row's and the band row steps with `--bd-fs` (batch R51). A
     13px here would have won over the variable and been the third layer
     that quietly disagrees — the trap R44 §2 and R49 both hit. */
  .today-list li{padding:8px 0}
  /* full bleed on a phone too (batch R48) — it was 10px in */
  .gcar-row{padding:4px env(safe-area-inset-right,0px) 14px
                    env(safe-area-inset-left,0px);
      gap:10px;scroll-padding-inline-start:env(safe-area-inset-left,0px)}
  .gc{flex-basis:88vw;padding:12px 12px 10px;block-size:calc((88vw - 26px) * .9067 + 56px)}
  /* a phone shows every third mood reading, smaller — thirty dots on a
     340px line sit on top of each other */
  .ig-plot .pt{width:8px;height:8px;margin:-4px 0 0 -4px}
  .ig-plot i.pt:not(:nth-of-type(3n)):not(:last-of-type){display:none}
}
/* the status cards: scrollable rows, a dot, a thirty-day strip, a count */
.gc-rows{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding-top:2px}
.gr{display:grid;grid-template-columns:10px minmax(64px,auto) 1fr auto;gap:10px;align-items:center;
      padding:7px 2px;border-top:1px solid var(--rule2);font-size:12.5px}
.gr:first-child{border-top:0}
.gr.gh{border-top:0;padding:4px 2px 6px;font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;letter-spacing:.12em;
      text-transform:uppercase;color:var(--dim)}
.gr.gh b,.gr.gh .n{font-weight:400;font-size:10px;color:var(--dim);letter-spacing:.12em}
.gr b{font-weight:500;color:var(--ink);white-space:nowrap;display:inline-flex;align-items:center;gap:6px}
.gr b .mk{width:14px;height:14px;display:inline-block}
.gr b .mk svg{width:100%;height:100%;display:block}
.gr .dot{width:8px;height:8px;border-radius:50%;background:var(--rule);cursor:pointer}
.gr .dot.ok{background:var(--up)}
.gr .dot.warn{background:var(--live)}
.gr .dot.bad{background:var(--down)}
.gr .strip{display:grid;grid-template-columns:repeat(30,1fr);gap:1.5px;height:10px;min-width:0}
.gr .strip i{border-radius:2px;background:var(--rule2);cursor:pointer;transition:transform .15s var(--ease)}
.gr .strip i:hover{transform:scaleY(1.4)}
.gr .strip i.u,.gr .strip i.h{background:var(--down)}
.gr .strip i.p{background:var(--live)}
.gr .n{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-variant-numeric:tabular-nums lining-nums;color:var(--ink);
      white-space:nowrap;text-align:end}
.gr .n s{text-decoration:none;color:var(--dim);font-size:10.5px;margin-inline-start:5px}
@media (max-width:620px){.gr{grid-template-columns:8px minmax(56px,auto) 1fr auto;gap:8px;font-size:12px}}
/* the widgets' art: every cell ends in a drawing of its own reading */
.wcell{display:flex;flex-direction:column}
.wcell .txt{display:flex;flex-direction:column;flex:1 1 auto}
.wart{margin-top:auto;padding-top:8px}
.wart svg{display:block;width:100%;height:40px;overflow:visible}
.wart .wring{height:44px;width:44px}
.warc .t{fill:none;stroke:var(--rule2);stroke-width:7;stroke-linecap:round}
.warc .f{fill:none;stroke:url(#bn-gg);stroke-width:7;stroke-linecap:round;stroke-dasharray:1;
      transition:stroke-dashoffset 1.2s var(--ease)}
.warc .n{fill:var(--ink);stroke:var(--surf);stroke-width:2}
.wring .t{fill:none;stroke:var(--rule2);stroke-width:5}
.wring .f{fill:none;stroke:var(--acc);stroke-width:5;stroke-dasharray:1;stroke-linecap:round;
      transform:rotate(-90deg);transform-origin:center;transition:stroke-dashoffset 1.2s var(--ease)}
.wcol rect{fill:var(--rule)}
.wcol rect.hi{fill:var(--down)}
.wlvl .g{fill:url(#bn-gl);opacity:.8}
.wlvl .tk{fill:var(--pap)}
.wlvl .m{fill:var(--ink);stroke:var(--surf);stroke-width:2;vector-effect:non-scaling-stroke}
.wdots circle{fill:var(--acc)}
.wsplit2 .l{fill:var(--up)}
.wsplit2 .s{fill:var(--down)}
.anim.ready .wcol rect{animation:bnrise .7s var(--ease) both}
.anim.ready .wdots circle{animation:bnpop .5s var(--ease) both}
.wcell{--art:1}
/* THREE STATES, AND THE BASE RULE IS THE THIRD ONE (R55 §5.3). A tile the
   classifier calls `flat` carries no `.up` / `.down`, so it lands here and
   washes toward `--dim`. There is deliberately no `.mm-t.flat` selector: it
   would only restate this rule, and two places to change a colour is how
   they drift. `band.map_flat_pct` decides which tiles get here. */
.mm-t rect{fill:color-mix(in srgb,var(--dim) calc(var(--v,0)*var(--kmax)),var(--tint));
      transition:filter .3s var(--ease)}
.mm-t.up rect{fill:color-mix(in srgb,var(--up) calc(var(--v,0)*var(--kmax)),var(--tint))}
.mm-t.down rect{fill:color-mix(in srgb,var(--down) calc(var(--v,0)*var(--kmax)),var(--tint))}
.mm-t:hover rect{filter:brightness(1.06)}
.mm-t text{font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;font-weight:600;fill:var(--ink);pointer-events:none}
.mm-t text.p{font-size:10px;font-weight:400;opacity:.85}
.anim.ready .mm-t{animation:bnpop .6s var(--ease) both;animation-delay:calc(var(--i,0)*12ms);
      transform-origin:center;transform-box:fill-box}
/* THE FIVE RULES THAT DREW THE LEAD AS A CARD ARE GONE (batch R45 §2).
   Above 561px `.grid.has-lead .card.lead` spanned two of three tracks,
   split itself into a drawn half and a written half, and set its headline
   to 24px/1.16. The lead is not in the grid any more — it is a slab under
   the price rail — so all five aimed at an element that no longer exists
   there, and the headline-size question they were answering is answered by
   `--ls-hd` below. Deleted rather than left: a rule matching nothing is the
   shape that makes the next reader believe the page still works that way. */

#overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);opacity:0;
         pointer-events:none;transition:opacity .2s;z-index:9}
/* parked off-screen with a transform, not right:-560px — the latter makes the
   document 560px wider than the viewport and scrollable sideways */
#flyout{position:fixed;top:0;inset-inline-end:0;width:min(540px,100vw);
        height:100vh;background:var(--panel);z-index:10;
        border-inline-start:1px solid var(--line);
        overflow-y:auto;overflow-x:hidden;transform:translateX(101%);
        transition:transform .25s ease}
body.open #overlay{opacity:1;pointer-events:auto}
body.open #flyout{transform:none}
#flyout .inner{padding:22px 26px 40px}
#flyout .close{position:sticky;top:14px;float:right;background:var(--card);
        border:1px solid var(--line);color:var(--text);width:34px;height:34px;
        font-size:16px;cursor:pointer}
#flyout svg{width:100%;height:auto;margin:10px 0 14px}
#flyout h2{font-size:22px;line-height:1.3;margin:6px 0 10px}
#flyout .body{font-size:15px;line-height:1.65;color:var(--body)}
#flyout .body p{margin:10px 0}
#flyout .body ul{margin:10px 0}
#flyout .body ul{margin-inline-start:20px}
#flyout .body a{color:var(--accent)}
/* The four constructs only a desk column's markdown produces — a heading, a
   numbered list and a quotation. `*{margin:0}` at the top of this stylesheet
   is what makes them need a rule at all: without one a `##` heading sits
   flush against the paragraph above it and a `>` quotation is a paragraph
   that looks exactly like the paragraphs either side of it, which is the
   author's formatting silently not arriving. Both bodies carry the same set
   at their own scale — the flyout is where the front page opens a column. */
#flyout .body h2,#flyout .body h3,#flyout .body h4{margin:20px 0 6px;
        line-height:1.35;color:var(--text)}
#flyout .body h2{font-size:18px}
#flyout .body h3{font-size:16px}
#flyout .body h4{font-size:14px;letter-spacing:.3px;color:var(--muted)}
#flyout .body ol{margin:10px 0;margin-inline-start:20px}
#flyout .body li{margin:5px 0}
#flyout .body blockquote{margin:14px 0;padding-inline-start:14px;
        border-inline-start:3px solid var(--accent-bg);color:var(--text)}
/* Code is the fifth, and it needs its own copy for a reason the four above
   do not: `pre` and `code` are styled UNSCOPED in ARTICLE_CSS, and the front
   page does not load that block at all — so a column opened in the flyout
   showed its fenced block as body-font prose with no ground under it. The
   fill is `--card`, not `--panel`: the flyout IS a panel. */
#flyout .body pre{background:var(--card);border:1px solid var(--line);
        padding:12px 14px;overflow-x:auto;font-size:12.5px;
        line-height:1.6;margin:10px 0}
#flyout .body code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
#flyout .body p code,#flyout .body li code{background:var(--card);
        border:1px solid var(--line);padding:1px 5px;
        font-size:.92em}
#flyout .disclaimer{color:var(--muted);font-size:12.5px}
#pill{position:fixed;left:50%;transform:translateX(-50%);bottom:26px;z-index:8;
      background:var(--accent);color:var(--on-accent);border:0;
      
      padding:11px 20px;font-size:13.5px;font-weight:400;cursor:pointer;
      }
#pill[hidden]{display:none}

/* ── REDESIGN 2026-09 · SECOND PASS ─────────────────────────────────────
   Kai, on the first pass: "not good enough … most things look like the old
   site … I don't like the coral and blueish dark … more smooth, soothing for
   the eye, nice buttons and icons, news cards look appealing." So every
   surface is re-drawn here, LAST in the cascade on every page: warm paper,
   raised rounded surfaces with a soft tinted shadow, pill controls, one
   teal-green accent, and motion on a single slow curve. The reading order
   of the page is untouched. */
:root{--ease:cubic-bezier(.32,.72,0,1);--r:16px;--r2:11px;
      --shadow:0 1px 2px rgb(0 0 0/.03),0 14px 34px -18px rgb(28 31 29/.22);
      --shadow-up:0 2px 4px rgb(0 0 0/.03),0 26px 52px -22px rgb(28 31 29/.34)}
::selection{background:color-mix(in srgb,var(--acc) 24%,transparent)}
body{font-variant-numeric:lining-nums}
/* header: a frosted bar, the wordmark heavy, links as pills */
header{background:color-mix(in srgb,var(--pap) 84%,transparent);
      -webkit-backdrop-filter:blur(18px) saturate(1.3);backdrop-filter:blur(18px) saturate(1.3);
      border-bottom:1px solid color-mix(in srgb,var(--rule) 70%,transparent)}
@supports not (backdrop-filter:blur(1px)){header{background:var(--pap)}}
header h1{font-size:22px;font-weight:800;letter-spacing:-.045em}
/* No `color` rule for the mark here any more: it had one because the
   four-cell mark was `currentColor`, and the cut b names `--acc` and
   `--pap` itself (MARK_CSS). A `color` here now would set nothing and
   read as the thing that paints the masthead's mark. */
.navlink{font-size:13.5px;font-weight:500;padding:6px 11px;border-radius:999px;line-height:1.2;
      transition:background-color .35s var(--ease),color .35s var(--ease)}
.navlink:hover{background:var(--tint);color:var(--ink)}
.cats a.navlink[aria-current]{background:var(--ink);color:var(--pap)}
.cats a.navlink[aria-current]::after{display:none}
.cats .grp{gap:2px}
.cats .grp+.grp{margin-inline-start:8px;padding-inline-start:8px;border-inline-start:1px solid var(--rule)}
.themebtn,.search .mag,.langpick>summary,.search.open .clear{
      width:36px;height:36px;padding:0;border-radius:999px;background:var(--tint);color:var(--ink);
      border:0;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
      transition:background-color .35s var(--ease),transform .45s var(--ease),color .35s var(--ease)}
.menubtn{border:0;border-radius:999px;background:var(--tint);color:var(--ink)}
.langpick>summary{width:auto;padding:0 12px 0 10px;gap:6px;font-weight:500;font-size:12.5px}
.themebtn:hover,.search .mag:hover,.langpick>summary:hover,.langpick[open]>summary,.search .clear:hover{
      background:var(--rule);color:var(--ink);transform:translateY(-1px)}
.themebtn:active,.search .mag:active{transform:scale(.94)}
.themebtn svg,.shareico{stroke-width:1.5}
.search input{background:var(--tint);border:0;border-radius:999px;height:36px;padding:0 14px;font-size:14px}
.search input:focus{border:0;box-shadow:0 0 0 2px color-mix(in srgb,var(--acc) 40%,transparent)}
.langmenu{border-radius:var(--r2);box-shadow:var(--shadow-up);border:1px solid var(--rule2);
      background:var(--surf);padding:6px}
.langmenu a{border-radius:8px}
.menubtn i{height:1.5px;border-radius:2px}
.navpanel{border-radius:0 0 var(--r) var(--r);box-shadow:var(--shadow-up);background:var(--surf)}
.navpanel a{border-radius:var(--r2)}
/* the grid and the cards: raised, rounded, lifting on hover */
.grid{gap:18px;border-top:0;padding-top:6px}
.card{background:var(--surf);border:1px solid var(--rule2);border-radius:var(--r);
      margin:0;padding:0;min-block-size:0;overflow:hidden;box-shadow:var(--shadow)}
.js .card.in{transition:transform .55s var(--ease),box-shadow .55s var(--ease),
      opacity .45s var(--ease),background-color .3s var(--ease)}
@media (hover:hover){.card:hover{transform:translateY(-3px);box-shadow:var(--shadow-up)}}
.card .top{margin:0;border-radius:0}
.card .pad{padding:14px 16px 18px;gap:8px}
.hd{font-size:18px;font-weight:700;letter-spacing:-.02em;line-height:1.22}
.card p{font-size:13.5px;line-height:1.55}
.meta{font-size:12px;gap:8px}
.badge{font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:10.5px;font-weight:700;letter-spacing:.1em;
      padding:3px 9px;border-radius:999px;background:var(--tint);color:var(--dim);line-height:1.3}
/* THREE TIERS, THREE COLOURS (the desk): flash red and blinking, story in
   the editorial teal, digest in the gold. The red is its own token so the
   two palettes each carry a red that clears 4.5:1 under paper-coloured type. */
:root{--flash:#F06A5F}
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){--flash:#C8322A}}
:root[data-theme="light"]{--flash:#C8322A}
.badge.story{background:var(--acc);color:var(--pap)}
.badge.flash{background:var(--flash);color:var(--pap);padding:3px 9px}
.badge.digest{background:var(--live-ink);color:var(--pap)}
.anim .badge.flash{animation:bnblink 1.8s var(--ease) infinite}
@keyframes bnblink{50%{opacity:.45}}
.badge.desk{border:1px solid var(--live);color:var(--live-ink);background:transparent;padding:2px 8px}
.chip-tl,.chip-live{border-radius:999px;background:var(--tint)}
.sharebtn{border-radius:999px}
.sharebtn:hover{background:var(--tint)}
#pill{border-radius:999px;box-shadow:var(--shadow-up);font-weight:600;background:var(--ink);color:var(--pap);
      transition:transform .35s var(--ease),background-color .3s var(--ease)}
#pill:hover{background:var(--acc);transform:translateX(-50%) translateY(-1px)}
.card.fresh{animation:bnfresh 2.6s var(--ease)}
@keyframes bnfresh{from{background-color:color-mix(in srgb,var(--acc) 18%,var(--surf))}}
.more button{background:var(--ink);color:var(--pap);border:0;border-radius:999px;
      padding:12px 24px;font-weight:600;font-size:14px;
      transition:transform .45s var(--ease),background-color .35s var(--ease)}
.more button:hover{background:var(--acc);color:var(--pap);transform:translateY(-1px)}
.more button:active{transform:scale(.97)}
/* the strip: eight raised cells */
/* AND THE THIRD PADDING RULE (batch R49). This one is later than the base
   rule and won above 720, so the strip measured 14px in at 1366 with the
   base rule already set to full bleed — the same three-layer trap R44 §2
   hit on this element's flex-basis, on a different property. Measured, not
   assumed: `padding-left` read 14px at 1366 after the first pass. */
.intel-row{border:0;--pkg:12px;gap:var(--pkg);
      padding-inline:env(safe-area-inset-left,0px)
                     env(safe-area-inset-right,0px)}
.wcell,.wcell:last-child,.wcell:nth-child(4n),.wcell:nth-child(n+5){
      border:1px solid var(--rule2);border-top:1px solid var(--rule2)}
.wcell{background:var(--surf);border-radius:var(--r2);box-shadow:var(--shadow);
      padding:14px 12px;transition:transform .5s var(--ease),box-shadow .5s var(--ease)}
@media (hover:hover){a.wcell:hover{background:var(--surf);transform:translateY(-2px);box-shadow:var(--shadow-up)}}
.wlab{font-size:10.5px;letter-spacing:.14em;font-weight:600;margin-bottom:8px}
.wnum{font-size:24px;font-weight:600;letter-spacing:-.02em}
.wnum s{font-size:12px}
.wsub{font-size:10.5px;letter-spacing:0;margin-top:3px}
.wbar,.wsplit{height:5px;border-radius:999px;overflow:hidden;margin-top:10px;background:var(--rule2)}
.wbar i,.wsplit i{border-radius:999px}
.wspark{block-size:22px;margin-top:8px}
.wspark polyline{stroke-linejoin:round;stroke-linecap:round}
.wdot{width:7px;height:7px}
/* THREE LAYERS SET THIS PROPERTY AND THE LAST ONE WINS — R44 §2 found that
   the hard way, changing only the upper rule and measuring exactly the same
   page. The two rules that used to live here and in the phone block are
   gone: the cell's width is `--w-cell` on the base rule and nothing
   re-declares it, so there is one declaration to read rather than three to
   put in agreement. `check_strip_peek` asserts that. */
/* the live band: a raised list, the time as a pill coloured by stream */
.band{border:0}
.band-head{flex-wrap:wrap;border-bottom:0;padding-block:12px 4px;gap:8px 12px}
.band-head h2{font-weight:700;letter-spacing:.14em;font-size:11px;color:var(--acc)}
.band-head h2::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--acc);
      box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 22%,transparent)}
.anim .band-head h2::before{animation:bnpulse 2.4s var(--ease) infinite}
@keyframes bnpulse{50%{transform:scale(1.4);opacity:.55}}
.band-list{background:var(--surf);border:1px solid var(--rule2);border-radius:var(--r);
      box-shadow:var(--shadow);overflow-x:hidden;overflow-y:auto;
      margin:0 max(20px,env(safe-area-inset-right),calc((100% - var(--wrap)) / 2))
             0 max(20px,env(safe-area-inset-left),calc((100% - var(--wrap)) / 2))}
/* THE BAND ROW'S SIZE IS A VARIABLE (batch R48). It is declared here rather
   than on `.bd` because the slab's live line is the SAME sentence off the
   same tape and Kai asked for the same size, and it is not inside `.band` —
   so a value only `.bd` could read would have to be copied, and a copy is
   what drifts. `.bd` and `.ls-live-t` both read this and nothing else does.
   Note the `.bd{font-size:17px}` in BAND_CSS is DEAD: this sheet is later,
   so 16/15 is what a reader has always been served. Measured, not read. */
:root{--bd-fs:16px}
.bd{padding:11px 18px;border-bottom:1px solid var(--rule2);font-size:var(--bd-fs);align-items:center;
      transition:background-color .3s var(--ease)}
.bd:hover{background:var(--tint)}
.bd time{font-size:12px;background:var(--tint);border-radius:999px;padding:2px 8px;color:var(--dim)}
.bd[data-stream="seconds"] time{color:var(--live-ink);background:color-mix(in srgb,var(--live) 18%,var(--tint))}
.bd[data-stream="minutes"] time{color:var(--acc);background:color-mix(in srgb,var(--acc) 14%,var(--tint))}
.bd .fig{font-size:16px;font-weight:600}
.band-down,.band-quiet{border-inline-start:0;margin:8px max(20px,env(safe-area-inset-right),calc((100% - var(--wrap)) / 2))
             8px max(20px,env(safe-area-inset-left),calc((100% - var(--wrap)) / 2));
      border-radius:var(--r2);background:color-mix(in srgb,var(--live) 12%,var(--tint));padding:10px 16px}
.band-new{border-radius:999px;background:var(--acc);color:var(--pap);border:0;box-shadow:var(--shadow-up)}
.band-new:hover{color:var(--pap);background:var(--ink)}
/* the price bar on every page that is not the front: a raised pill row
   inside the wrap, its cells reading as they did */
.tickerbar{margin:10px auto 0;max-width:var(--wrap);border:1px solid var(--rule2);
      border-radius:var(--r2);background:var(--surf);box-shadow:var(--shadow)}
/* the article page's share row, citation chip and integrity panel */
.share input{border:0;border-radius:999px;background:var(--tint);padding:10px 14px}
.share button,.share a.btn{border-radius:999px;background:var(--ink);color:var(--pap);
      transition:background-color .35s var(--ease),transform .45s var(--ease)}
.share button:hover,.share a.btn:hover{background:var(--acc);color:var(--pap);transform:translateY(-1px)}
.share a.alt{border-radius:999px;border:1px solid var(--rule2);background:var(--surf)}
.share a.alt:hover{border-color:var(--acc)}
.cite-id code{border-radius:8px;background:var(--tint)}
.cite-id button{border-radius:999px;border:1px solid var(--rule2);background:var(--surf)}
.integ{border:1px solid var(--rule2);border-radius:var(--r);background:var(--surf);box-shadow:var(--shadow)}
.kitlink a{border-radius:999px}

/* the flyout and the footer */
#flyout .inner{border-radius:var(--r) 0 0 var(--r)}
#flyout .close{border-radius:999px;background:var(--tint)}
footer{background:var(--tint);border-top:0;border-radius:var(--r) var(--r) 0 0;
      margin:44px auto 0;max-width:calc(var(--wrap) + 40px);padding:36px 24px 44px;
      font-size:12.5px;line-height:1.8}
footer a{text-decoration:none;border-bottom:1px solid var(--rule);padding-bottom:1px}
footer a:hover{color:var(--acc);border-color:var(--acc)}
/* …except the brand marks, which are not words and take no underline. The
   rule above is written for text links and would draw a line under a 15px
   glyph. */
footer .socs a.soc{border-bottom:0;padding-bottom:0}
/* A PHONE IS NOT A SHRUNKEN DESKTOP AND NOT A LOUDER ONE EITHER (Kai: "the
   fonts are too big on mobile"). Every register steps down one notch. */
@media (max-width:720px){
  header h1{font-size:19px}
  .navpanel a{font-size:15px;min-height:44px}
  .hd{font-size:17px}
  .card p{font-size:13px}
  .card .pad{padding:12px 14px 16px}
  .meta{font-size:11px}
  .grid{gap:14px}
  /* the row steps down one notch on a phone, and the live line steps with
     it because both read `--bd-fs` (batch R48). */
  :root{--bd-fs:15px}
  .bd{padding:10px 14px}
  .bd .tx{-webkit-line-clamp:3;line-clamp:3}
  .bd .fig{font-size:15px}
  .band-head .upd{font-size:12px}
  .wnum{font-size:21px}
  .wlab{font-size:10px}
  .more button{font-size:13px;padding:10px 20px}
  footer{margin-top:32px;font-size:12px}
}
/* ── the menu (the desk: "fix the boring menu/nav bar") ──────────────────
   Desktop: a quiet editorial masthead — the wordmark larger, the links as
   plain words with room between them, the two groups parted by a dot, the
   current page underlined in the accent, the controls round and light.
   Phone: the drawer is a two-column grid of pill chips rather than a column
   of 48px rows, so the whole menu fits above the fold. */
header{padding:10px 24px}
header h1{font-size:24px}
header h1 .mk-byte{inline-size:1.05em;block-size:1.05em;margin-inline-end:.35em}
.cats{background:transparent;border-radius:0;padding:0;gap:0}
.cats .grp{gap:18px}
.cats .grp+.grp{margin-inline-start:18px;padding-inline-start:18px;border-inline-start:0;position:relative}
.cats .grp+.grp::before{content:"";position:absolute;inset-inline-start:-2px;top:50%;width:4px;height:4px;
      margin-top:-2px;border-radius:50%;background:var(--rule)}
.navlink{padding:4px 0;border-radius:0;font-size:13.5px;font-weight:500;letter-spacing:.005em;
      color:var(--dim);position:relative;background:transparent}
.navlink:hover,.cats .navlink:hover,header>a.navlink:hover{background:transparent;color:var(--ink)}
.cats a.navlink[aria-current]{background:transparent;color:var(--ink);box-shadow:none}
.cats a.navlink::after{bottom:-6px;height:2px;background:var(--acc);border-radius:2px}
.cats a.navlink[aria-current]::after{display:block;transform:scaleX(1)}
header>a.navlink{background:transparent;padding:4px 0;margin-inline:10px 6px}
/* DYNAMIC (the desk): a tinted pill GLIDES under whichever link the pointer
   is on, the underline grows in on hover, a press dips the word. */
.cats{overflow:visible;position:relative;isolation:isolate;margin-inline-start:28px}
.nav-glide{position:absolute;top:50%;inset-inline-start:0;width:100px;height:30px;margin-top:-15px;
      border-radius:999px;background:var(--tint);opacity:0;transform-origin:left center;
      transition:transform .4s var(--ease),opacity .25s var(--ease);pointer-events:none;z-index:0}
[dir="rtl"] .nav-glide{transform-origin:right center}
.nav-glide.on{opacity:1}
.cats .navlink,.cats .navmenu{position:relative;z-index:1}
.cats a.navlink::after{transform-origin:center;transition:transform .3s var(--ease)}
.cats a.navlink:hover::after{transform:scaleX(1);background:var(--rule)}
.cats a.navlink[aria-current]:hover::after{background:var(--acc)}
.navlink{transition:color .25s var(--ease),transform .2s var(--ease)}
.navlink:active,.navmenu>summary:active{transform:translateY(1px) scale(.96)}
.navmenu{position:relative;display:inline-block}
.navmenu>summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:3px}
.navmenu>summary::-webkit-details-marker{display:none}
.navmenu .i-chev{width:12px;height:12px;transition:transform .3s var(--ease);opacity:.7}
.navmenu[open] .i-chev{transform:rotate(180deg)}
.navmenu[open]>summary,.navmenu.here>summary{color:var(--ink)}
.navmenu.here>summary::after{content:"";position:absolute;left:0;right:14px;bottom:-6px;height:2px;
      border-radius:2px;background:var(--acc)}
.navmenu-panel{position:absolute;top:calc(100% + 12px);inset-inline-start:-10px;z-index:12;
      background:var(--surf);border:1px solid var(--rule2);border-radius:var(--r2);
      box-shadow:var(--shadow-up);padding:8px;display:grid;grid-template-columns:1fr 1fr;gap:2px;
      min-width:320px}
.navmenu-panel a.navlink{padding:9px 12px;border-radius:8px;font-size:13.5px;font-weight:500;color:var(--ink);
      text-decoration:none;white-space:nowrap;display:block}
.navmenu-panel a.navlink::after{display:none}
.navmenu-panel a:hover{background:var(--tint);color:var(--acc)}
.navmenu-panel a[aria-current]{background:var(--tint);color:var(--acc)}
.anim .navmenu[open] .navmenu-panel{animation:bnrise .3s var(--ease)}
.navpanel .grp-lab{grid-column:1/-1;font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;letter-spacing:.14em;
      text-transform:uppercase;color:var(--dim);padding:8px 4px 0}
.langpick>summary{background:transparent;border:1px solid var(--rule);height:34px;
      font-size:11px;letter-spacing:.1em;padding:0 12px 0 10px}
.langpick>summary:hover,.langpick[open]>summary{background:var(--tint);transform:none}
.themebtn,.search .mag{width:34px;height:34px;background:transparent;border:1px solid var(--rule)}
.themebtn:hover,.search .mag:hover{background:var(--tint);transform:none}
.navpanel:not([hidden]){display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:12px 14px 14px}
.navpanel .grp{display:contents}
.navpanel .grp+.grp{border:0;margin:0;padding:0}
.navpanel a{min-height:40px;padding:6px 12px;border-radius:999px;background:var(--tint);
      font-size:14px;justify-content:center;text-align:center;color:var(--ink)}
.navpanel a[aria-current]{background:var(--ink);color:var(--pap)}
.navpanel a[aria-current]::before{display:none}
.navpanel .langrow{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:6px;justify-content:center;padding-top:4px}
.navpanel .langrow a{min-height:32px;font-size:12.5px;padding:4px 10px}
/* One account, one row — batch R64 §B2. Full width because the label is a
   sentence ("Follow on Telegram") rather than the one word the category
   pills carry, and a sentence in a half-width pill wraps at 360. */
.navpanel .socrow{grid-column:1/-1;display:flex;align-items:center;
      justify-content:center;gap:8px;min-height:40px;padding:6px 12px;
      border-radius:999px;background:var(--tint);color:var(--ink);
      font-size:14px;text-decoration:none}
.navpanel .socrow:hover{background:var(--ink);color:var(--pap)}
.navpanel .socrow .socg{inline-size:1.1em;block-size:1.1em}

/* Three named states, one row — batch R64 §B1. `grid-column:1/-1` because
   the panel is a two-column grid and this is one control, not two rows. */
.navpanel .themerow{grid-column:1/-1;display:grid;
      grid-template-columns:repeat(3,1fr);gap:6px;padding-top:4px}
.navpanel .themerow button{min-height:40px;padding:6px 8px;border-radius:999px;
      background:var(--tint);border:1px solid transparent;color:var(--ink);
      font-family:'BN Head',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif;font-size:12.5px;cursor:pointer}
/* THE ACTIVE ONE, OFF THE ATTRIBUTE AND NOT OFF `aria-pressed`: the panel is
   in the DOM from first paint and `bnThemeSync` cannot run until the body's
   end, so during that gap the only thing that knows the reader's stored
   choice is the attribute THEME_INIT already set. Auto is the ABSENCE of it,
   which is why it needs `:not()` and not a value. */
:root:not([data-theme]) .navpanel .themerow button[data-v="auto"],
:root[data-theme="light"] .navpanel .themerow button[data-v="light"],
:root[data-theme="dark"] .navpanel .themerow button[data-v="dark"]{
      background:var(--ink);color:var(--pap)}
/* ── the price bar: a raised pill row, always moving ─────────────────── */
.tickerbar{margin:0;max-width:none;border:0;border-block:1px solid var(--rule2);
      border-radius:0;background:var(--surf);box-shadow:none;font-size:12px}
.tk-half{gap:8px;padding:6px 10px}
.tk{background:var(--tint);border-radius:999px;padding:4px 10px;gap:6px;align-items:center}
.tk b{font-weight:600;letter-spacing:.06em;font-size:11px}
.tk .c{border-radius:999px;padding:1px 6px;background:color-mix(in srgb,var(--dim) 12%,transparent);font-size:11px}
.tk .c.up{background:color-mix(in srgb,var(--up) 14%,transparent)}
.tk .c.down{background:color-mix(in srgb,var(--down) 14%,transparent)}
.tk-spark{inline-size:40px;block-size:14px}
/* ── scroll to top ───────────────────────────────────────────────────── */
#totop{position:fixed;inset-inline-end:16px;inset-block-end:16px;width:44px;height:44px;
      border-radius:999px;background:var(--ink);color:var(--pap);border:0;cursor:pointer;
      display:inline-flex;align-items:center;justify-content:center;z-index:6;
      box-shadow:var(--shadow-up);opacity:0;transform:translateY(8px);pointer-events:none;
      transition:opacity .35s var(--ease),transform .45s var(--ease),background-color .35s var(--ease)}
#totop svg{width:20px;height:20px}
#totop.on{opacity:1;transform:none;pointer-events:auto}
#totop:hover{background:var(--acc)}
/* ── the phone: no wasted height between the masthead and the bar, the
   band and the strip; cells as tall as their content, not as their row ── */
@media (max-width:720px){
  header{padding-block:6px}
  main{padding-inline:10px}
  .band-list,.band-down,.band-quiet{margin-inline:10px}
  .band-head{padding-inline:12px}
  /* full bleed here too (R49): it was 8px, which is a page edge the strip
     does not share with anything else on it. */
  .intel-row{padding-inline:env(safe-area-inset-left,0px)
                            env(safe-area-inset-right,0px)}
  .grid{gap:12px}
  .tickerbar{margin:0}
  .band-head{padding-block:8px 2px}
  .intel{margin-bottom:10px}
  /* EVERY WIDGET THE SAME HEIGHT (batch R49). `align-items:start` is what
     made a phone's tiles ragged — measured on the live page, nine tiles at
     390 came back 88.8, 130.8 and 140.8px tall, because a tile with no bar
     and no sparkline is a shorter box and `start` lets it be one. The
     desktop grid was uniform only because a grid row stretches its cells;
     the default `stretch` on this flex row is the same rule, said once and
     applying at every width. */
  .wcell{padding:12px 12px}
  .wart svg{height:34px}
  .grid{padding-top:2px}
  .ig-chart{height:96px}
}
/* THE SET A RANKING OPENS ON: the BYTE 100, with the wider set one tap
   away. State is the URL fragment and CSS :target — no script, no store;
   the anchors precede the control and the blocks because ~ looks forward */
.setw{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
.setw>i,.prose>i[id^="set-"]{display:block;height:0;margin:0;overflow:hidden}
.setpick{display:flex;gap:6px;margin:0 0 8px;padding:0;list-style:none}
.prose .setpick{margin:12px 0 4px}
.setpick a{display:inline-block;font-family:'BN Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;font-weight:500;line-height:1;
      padding:6px 10px;border-radius:999px;border:1px solid var(--rule);color:var(--dim);
      text-decoration:none;transition:background .35s var(--ease),color .35s var(--ease),border-color .35s var(--ease)}
.setpick a:hover{color:var(--ink);border-color:var(--dim)}
.setpick a.s100,#set-500:target~.setpick a.s500,#set-all:target~.setpick a.sall{
      background:var(--acc);border-color:var(--acc);color:var(--pap)}
#set-500:target~.setpick a.s100,#set-all:target~.setpick a.s100{
      background:transparent;border-color:var(--rule);color:var(--dim)}
.setw .mv[data-set="500"]{display:none}
#set-500:target~.mv[data-set="500"]{display:grid}
#set-500:target~.mv[data-set="100"]{display:none}
.prose tr.o,.prose .note.sall{display:none}
#set-all:target~.tw tr.o{display:table-row}
#set-all:target~.note.sall{display:block}
#set-all:target~.note.s100{display:none}
