/* smart-mill.css — page-only. Everything global lives in tokens/base/shell.
   Composition: THE DECIMAL OUTLINE (Linear, plan §07) — the eight stations numbered
   1.0–1.7 as a technical outline, with a sticky rail, never as eight identical cards.
   Contrast: every colour pair below was computed against its real ground before use.
   (History: --ink-ghost was #4A5A6C = 2.60:1 on Deep Water when this page was written, so
   nothing here uses it for text. tokens.css raised it to #7E99B7 on 2026-08-22 and it now
   clears AA on all four grounds — the avoidance is no longer necessary, only harmless.) */

/* --hdr (the real sticky-header height per breakpoint) is declared ONCE in shell.css.
   This page is a CONSUMER of it in two places — the rail's sticky offset below, and
   the station scroll-margins that shell.css's `[id]` rule now supplies. The values
   measured on this page at 11 widths (65 / 80.19 / 83.8) agreed with Home's to the
   pixel, which is why one declaration serves both. */

/* ─── hero ─────────────────────────────────────────────────────────────── */
.hero{border-top:0;padding-top:clamp(46px,6vw,84px)}
.hero .grid{display:grid;gap:clamp(28px,4vw,58px);grid-template-columns:minmax(0,1fr)}
@media (min-width:980px){.hero .grid{grid-template-columns:minmax(0,1.02fr) minmax(0,.98fr);align-items:end}}
.hero h1{font-size:clamp(38px,6.6vw,76px);margin:20px 0 0;max-width:14ch}
.hero .sub{margin:22px 0 0;max-width:52ch;font-size:clamp(16.5px,1.5vw,19px);color:var(--ink-muted)}
/* ABSOLUTE BAN — side-stripe borders. A `border-left` thicker than 1px used as a coloured
   accent on a card, a list item, a callout or an alert is the first item on the DON'T list
   in the impeccable skill, and it is the one AI tell this site was still carrying in six
   places. The prescribed rewrites are full borders, background tints, leading
   numbers/icons, or nothing, and each of the six below takes whichever of those the
   surrounding component already uses. */
/* Was a hairline box with one thickened gold edge. A chip is small enough to take the
   accent all the way round at 1px, which is a full border rather than a stripe. */
/* SENTENCE CASE, BECAUSE THIS CHIP IS A SENTENCE. Measured 2026-08-27 in Chromium at
   1440x900: 101 painted characters of uppercase mono at .11em, wrapping to two lines, in
   the hero — the longest true all-caps run on the site and the only one past 70 characters.
   Uppercase is the right dress for a LABEL, where a reader takes the whole word as one
   shape; past about forty characters they have to read it, and caps removes the ascenders
   and descenders that reading uses. Half of this one is proper nouns — Minsawi Industries,
   Kuala Kangsar, a minister's portfolio — and caps erases the capitalisation that says so.
   news.css `.src-tag` already made exactly this call for the masthead list, in those words.
   The stamp survives: it was never the caps, it is the gold hairline box, the mono, and the
   gold `b`. Tracking drops .11em -> .02em because tracking is a caps device; at sentence
   case it only pulls the words apart. */
.chip{display:inline-flex;align-items:center;gap:12px;margin-top:26px;padding:10px 16px;
  border:1px solid rgba(255,214,88,.5);border-radius:2px;
  font-family:var(--f-mono);font-size:13px;letter-spacing:.02em;
  color:var(--ink-muted);line-height:1.45;max-width:100%}
.chip b{color:var(--signal-gold);font-weight:600}
/* on a phone the two halves of the chip were squeezed into two narrow columns;
   below 560 they read as two stacked lines, which is what they are */
@media (max-width:560px){.chip{display:flex;flex-direction:column;align-items:flex-start;gap:7px}}
/* shell.css sets `footer a{display:block}`, which pushed the legal row's Privacy link
   onto a line of its own above the city list. Reported to the Home lane; scoped here. */
footer .legal a{display:inline;padding:0;font-size:inherit;font-family:inherit;
  border-bottom:1px solid var(--hairline-strong)}
figure{margin:0}
figure img{width:100%;height:auto;border-radius:2px}
/* Sentence case per .src-line in proof.css. This is the longest all-caps run on the site:
   407 characters on ms/kilang-pintar, 245 on smart-mill, both full paragraphs describing
   what a screen shows. */
.cap{margin:10px 0 0;font-family:var(--f-mono);font-size:12.5px;letter-spacing:.02em;
  color:var(--ink-subtle);line-height:1.6}

/* ─── the decimal outline: rail + stations ─────────────────────────────── */
.stations{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(30px,4vw,64px);align-items:start}
@media (min-width:901px){.stations{grid-template-columns:224px minmax(0,1fr)}}

.rail-h{font-family:var(--f-mono);font-size:12.5px;letter-spacing:.19em;text-transform:uppercase;
  color:var(--ink-subtle);margin:0 0 12px}
.rail ol{list-style:none;margin:0;padding:0;border-top:1px solid var(--hairline-strong)}
.rail li{border-bottom:1px solid var(--hairline)}
.rail a{display:flex;gap:10px;align-items:baseline;padding:10px 8px 10px 10px;
  font-family:var(--f-mono);font-size:12.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-subtle);border-left:2px solid transparent;line-height:1.45;
  transition:color var(--d2) var(--e-quad),border-color var(--d2) var(--e-quad)}
.rail a .n{color:var(--crude-amber);flex:none;font-weight:600}
@media (hover:hover) and (pointer:fine){.rail a:hover{color:var(--ink)}}
.rail a[aria-current="true"]{color:var(--signal-gold);border-left-color:var(--signal-gold)}
.rail a[aria-current="true"] .n{color:var(--signal-gold)}
/* the third rail state. `[aria-current]` is the reader's position, owned by the
   scroll-spy; `.is-peer` is "the drawing above is being pointed at this row" and
   is set by assets/js/fx-plant.js. Amber, not gold, so it can never be mistaken
   for the current chapter. */
.rail a.is-peer{color:var(--ink);border-left-color:var(--crude-amber)}
@media (prefers-reduced-motion:reduce){.rail a{transition:none}}
@media (min-width:901px){.rail{position:sticky;top:calc(var(--hdr) + 22px)}}
@media (max-width:900px){
  .rail ol{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--hairline-strong)}
  .rail li:nth-child(odd){border-right:1px solid var(--hairline)}
  .rail a{padding:11px 8px}
}
@media (max-width:400px){.rail ol{grid-template-columns:1fr}.rail li:nth-child(odd){border-right:0}}

.note{max-width:64ch;font-size:14.5px;color:var(--ink-muted);border-left:1px solid var(--hairline-strong);
  padding-left:16px;margin:22px 0 clamp(30px,4vw,46px)}
.note b{color:var(--ink);font-weight:600}

/* ─── one station block ────────────────────────────────────────────────── */
/* scroll-margin-top removed: shell.css scopes it to `[id]`, which is a strict
   superset here — all 8 .st blocks carry an id (measured), and `[id]` additionally
   fixes #closed-loop, which this narrower rule never reached. */
.st{padding:clamp(38px,5vw,64px) 0;border-top:1px solid var(--hairline)}
.st:first-of-type{border-top:0;padding-top:0}
.st h3{font-size:clamp(25px,3.3vw,40px);max-width:15ch}
.st h3 .n{display:block;font-family:var(--f-mono);font-size:13px;font-weight:600;letter-spacing:.2em;
  color:var(--crude-amber);margin-bottom:12px;letter-spacing:.2em}
.st .lede{margin:16px 0 0;max-width:50ch;color:var(--ink-muted);font-size:16px}
.caps{list-style:none;margin:24px 0 0;padding:0}
.caps li{position:relative;padding:11px 0 11px 22px;border-top:1px solid var(--hairline);
  color:var(--ink-muted);font-size:15.5px;line-height:1.5}
.caps li::before{content:"";position:absolute;left:0;top:20px;width:10px;height:1px;background:var(--crude-amber)}
/* Sentence case per .cap above — 84 characters of sentence at .13em tracking. */
.verb{margin:16px 0 0;font-family:var(--f-mono);font-size:12px;letter-spacing:.02em;
  line-height:1.55;color:var(--ink-subtle)}

/* asymmetry — five different shapes, deliberately not a card grid */
.st--media{display:grid;gap:clamp(24px,3.4vw,48px);grid-template-columns:minmax(0,1fr)}
@media (min-width:820px){
  .st--media{grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);align-items:start}
  .st--flip .fig{order:2}
}
.st--inset{padding-left:0}
@media (min-width:820px){.st--inset .body{margin-left:22%;max-width:40rem}}
@media (min-width:820px){.st--split .caps{display:grid;grid-template-columns:1fr 1fr;column-gap:36px}
  .st--split .caps li:nth-child(2){border-top:1px solid var(--hairline)}
  .st--split h3{max-width:22ch}}
@media (min-width:820px){.st--plain .body{max-width:38rem}}
.pair{display:grid;gap:clamp(24px,3.4vw,44px);grid-template-columns:minmax(0,1fr);
  padding:clamp(38px,5vw,64px) 0;border-top:1px solid var(--hairline)}
@media (min-width:820px){
  .pair{grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);align-items:start}
  /* 1.5 is stepped down the page rather than set level with 1.4. Two reasons, and the
     second is not cosmetic: it deepens the asymmetry, AND it gives the two stations
     distinct scroll positions. Set level, they shared one scroll band, so no outline
     rail could ever tell them apart and 1.5 could never become the current chapter. */
  .pair > *:nth-child(2){margin-top:clamp(52px,7vw,104px)}
  /* The value is a straight trade and worth naming: the stagger IS 1.5 Boiler's exclusive
     scroll band in the rail, measured 1:1 (86px stagger -> 75px band, 124px -> ~120px).
     Bigger reads better in the outline and worse as layout, because 1.4 Kernel Plant is
     the shorter column and the difference lands as bare navy beneath it. 104px is the
     middle: ~90-104px of exclusive band at desktop against ~240px of tail whitespace. */
}
.pair .st{padding:0;border-top:0}
.pair .st h3{font-size:clamp(23px,2.6vw,30px)}

/* the two callout stations */
.st--callout .body{background:var(--panel-navy);border:1px solid var(--hairline);border-radius:2px;
  padding:clamp(22px,3vw,34px)}
.st--callout .caps li:first-child{border-top:0}
/* Was a 2px amber left stripe. `.caps li::before` twenty lines above already solves this
   exact problem on this exact page with a 10px amber TICK, so the hook takes the same mark
   instead of inventing a second idiom for "amber marks the thing to read". */
.hook{position:relative;margin-top:24px;padding:3px 0 3px 26px}
.hook::before{content:"";position:absolute;left:0;top:.72em;width:14px;height:1px;
  background:var(--crude-amber)}
.hook+.hook{margin-top:18px}
.hook h4{font-family:var(--f-body);font-weight:700;font-size:15.5px;letter-spacing:-0.0017em;
  line-height:1.35;color:var(--signal-gold)}
/* MEASURE. `.hook p` had no cap, which was invisible while every `.hook` sat in a narrow
   station column — `#s-1-6` measures 30.9ch. `#s-1-7` puts the same component at the full
   text width, and there the paragraphs run 81.0ch, 98-100 characters to a line, on
   smart-mill and kilang-pintar alike at 1280, 1440 and 1920.
   Attribution, so nobody re-litigates it: this is NOT caused by the measure caps added
   elsewhere in this pass. Control run 2026-08-27 — all four of those caps reverted on a
   scratch copy of this exact tree, `#s-1-7 .hook > p` still measured 81.0ch. It arrived
   with the section itself; the component simply never had a measure to begin with.
   65ch is 596px at 15px, ~72 characters — the same cap `.pr-widebody li` uses for the
   same face and size. `#s-1-6` is below it already, so nothing there moves. */
.hook p{margin:7px 0 0;max-width:65ch;font-size:15px;color:var(--ink-muted);line-height:1.5}

/* ─── the closed loop ──────────────────────────────────────────────────────
   FX-03 replaced the four-across card row with a drawn cycle beside a numbered
   ledger. The ring, the ledger and the return line all live in
   assets/css/fx-plant.css — kept in ONE file so the effect is auditable in one
   place, rather than half here and half there. What stays here is the section's
   own spacing and the verdict beneath it. */
.loopwrap{margin-top:clamp(30px,4vw,52px)}
.verdict{margin:clamp(28px,3.6vw,44px) 0 0;max-width:62ch}
/* line-height 1.25 -> 1.34. This is not a pull quote; it is a three-sentence LEDE set in
   the display face, and `max-width:62ch` says so — it runs 3 lines in English and 4 in
   Malay at 44 characters per line, which is linear reading, not a poster. At 31px the
   1.25 gap left 7.7px between the descenders of "closed-loop" / "Every" and the ascenders
   of the line below, and the block read visibly more compressed than the 16px/1.6 body
   paragraph sitting directly under it. 1.34 is the editorial value for display type used
   at reading length and closes the mismatch. The poster on this same page — `.pull .q`,
   54px in a 16ch column — is DELIBERATELY tighter at 1.08 and is left alone; see the note
   there. Verified at 2x, 1440 and 390, in both languages. */
.verdict p{font-family:var(--f-display);font-size:clamp(21px,2.5vw,31px);line-height:1.34;
  letter-spacing:-0.0068em;color:var(--ink)}
.verdict p em{font-style:normal;color:var(--signal-gold)}
.verdict .after{font-family:var(--f-body);font-size:16px;line-height:1.6;color:var(--ink-muted);
  letter-spacing:0;margin-top:16px}

/* ─── the honest-engineer band (paper) ─────────────────────────────────── */
.paper a{color:#10598E}
.paper .btn-secondary{color:#16202B;border-color:rgba(22,32,43,.3)}
@media (hover:hover) and (pointer:fine){
  .paper .btn-secondary:hover{border-color:#8A5E14;color:#8A5E14}}
.paper .cap{color:#4A5A6C}
.honest{display:grid;gap:clamp(28px,4vw,56px);grid-template-columns:minmax(0,1fr)}
@media (min-width:900px){.honest{grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);align-items:start}}
.pull{margin:26px 0 0;border:0;padding:0}
/* line-height 1.08 IS DELIBERATE AND STAYS. A generic readability scan flags it — the
   1.3 floor those scans use is a BODY floor, and this is a 54px poster in a 16ch column,
   where tight leading is what makes four short lines read as one stacked slab instead of
   four paragraphs. It was checked in the pixels rather than in the number, at 2x, in both
   languages: English runs 2 lines, Malay 4 ("AI tidak dapat / menyelamatkan /
   kejuruteraan yang / lemah."), and in the Malay stack every descender clears the line
   below — the 'p' of "dapat" ends above the x-height of "menyelamatkan", the 'y' tail
   passes left of the 'j' in "kejuruteraan", and the 'g' of "yang" is far right of where
   "lemah." begins. No collision, and the slab is the design. The LEDE on this page
   (`.verdict p`) is a different job and was opened to 1.34; see the note there. */
.pull .q{font-family:var(--f-display);font-size:clamp(30px,4.6vw,54px);line-height:1.08;
  letter-spacing:-0.0085em;color:#0E1922;max-width:16ch;margin:0}
.pull footer{padding:0;border-top:0;margin-top:20px;color:#3C4C5E;font-size:14.5px;font-family:var(--f-body)}
.pull cite{font-style:normal;color:#16202B;font-weight:600}
.pull .also{font-family:var(--f-body);margin:18px 0 0;font-size:16px;color:#3C4C5E;max-width:46ch}
.answer{border-top:1px solid rgba(22,32,43,.18);padding-top:22px}
.answer h3{font-size:clamp(21px,2.4vw,27px);color:#0E1922;max-width:20ch}
.answer p{margin-top:14px;font-size:16px;color:#3C4C5E;max-width:46ch}
/* .06em -> .02em. This is the `.src` source-note role, and every other instance of it on
   the site is set at .02em: `.stat .src` and `.pband .src` in base.css, `.src` in news.css,
   `.creds .src` in home.css. This one was the outlier, and it carries the LONGEST source
   sentence on the site — 165 characters over 8 line boxes at 1440, measured on the rendered
   page. Label tracking is what makes a three-word eyebrow scan; on a running sentence it
   pulls the words apart faster than it pulls the letters together, which is the one thing a
   citation cannot afford. The label voice is untouched everywhere it is actually a label. */
.answer .src{margin-top:16px;font-family:var(--f-mono);font-size:12.5px;letter-spacing:.02em;
  color:#4A5A6C;line-height:1.65}
.answer .src a{border-bottom:1px solid rgba(22,32,43,.28)}

/* ─── close ────────────────────────────────────────────────────────────── */
.close h2{font-size:clamp(32px,4.6vw,56px);max-width:17ch}
.close p{margin-top:20px;max-width:48ch;color:var(--ink-muted);font-size:17px}
.actions{display:flex;flex-wrap:wrap;gap:14px;margin-top:30px}
/* the separators were literal "//" between inline links, so a wrap could start a line
   with a stray "//". They are pseudo-elements on each item now — a separator can only
   ever lead the item it belongs to. */
/* SPECIFICITY: `.close p` above is (0,1,1) and this rule was (0,1,0), so the block asked
   for 13px and rendered at 17px at every viewport — measured 17px computed, with .11em of
   that larger size giving 1.87px of tracking rather than the 1.43px the label voice was
   drawn at. Scoping to `.close .contact` (0,2,0) makes the declared size the rendered one.
   The list is the only place on the page where a MACHINE-READABLE string is set in the
   label voice, and the two treatments that make a label legible make a literal string
   worse. `INFO@AIREI.COM.MY` is not the address anyone types, and .11em pushes the `@` and
   the two `.` far enough from their neighbours that the address has to be reassembled by
   eye before it can be copied. The location keeps the caps — it is a label, and reading it
   is all it is for. */
.close .contact{margin-top:34px;padding-top:22px;border-top:1px solid var(--hairline);
  display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 16px;
  font-family:var(--f-mono);font-size:13px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--ink-subtle);line-height:2}
/* the address and the number are quoted as themselves: no case change, and the house
   mono-PROSE tracking (.02em, as every `.src` on the site uses) instead of the label .11em,
   so the digit groups and the domain parts hold together. */
.close .contact a{text-transform:none;letter-spacing:.02em}
/* the size correction above took the line box from 34px to 26px, and with it the tap target
   on the two links a phone actually uses: measured 35px -> 27px at 390px with has_touch —
   and 27px was already short of the 44px minimum before the correction made it shorter.
   9px of padding-block takes both links to 45px, measured.
   THIS GROWS THE BLOCK, and the first draft of this comment claimed it did not. These links
   are FLEX ITEMS of `.contact`, not inline boxes, so padding is layout-affecting: measured
   at 390px with has_touch, the row goes 80px -> 98px and the document 19968px -> 19986px.
   That is the intended trade and the house precedent behaves the same way — `.fits .go` in
   home.css takes a coarse-pointer min-height of 44px and grows too. It is scoped to
   `pointer:coarse`, so no mouse layout changes at all. */
@media (pointer:coarse){.close .contact a{padding-block:9px}}
.contact > * + *::before{content:"// ";color:var(--mill-steel)}
.contact a{color:var(--ink-subtle);border-bottom:1px solid var(--hairline-strong)}
@media (hover:hover) and (pointer:fine){.contact a:hover{color:var(--ink)}}

.pull .attrib{font-family:var(--f-body);margin:20px 0 0;color:#3C4C5E;font-size:14.5px;
  line-height:1.6;max-width:44ch}
.answer figure{margin:24px 0 0}

/* ─── SHELL CORRECTIONS — MOVED to shell.css ───────────────────────────────
   The skip-link outline suppression, the footer-column orphan fix and the whole
   masthead-fit block were applied here page-scoped so a parallel lane was not edited
   underneath this one. That was right while the lanes ran. They now live once, in
   shell.css, which is the file that declares the masthead and the footer in the first
   place — with this lane's measurements (390px scrollWidth 504 vs client 390; the
   901-1359 lockup needing ~74px it does not have) carried across intact. */


/* ─── print ────────────────────────────────────────────────────────────────
   base.css puts `break-inside:avoid` on every <section>. That is right for a short
   band and a no-op with side effects for a section taller than a page: this page's
   stations and closed-loop sections both are, and the residue was a print page
   carrying three orphaned lines and nothing else (measured: page 9 of 12, 175 chars).
   The correct granularity is to let the long sections break and protect the ATOMS.

   THE SAME TRAP WAS STILL SET ONE LEVEL DOWN. Measured 2026-08-23 on the A4 render
   (48dpi raster, largest CONTIGUOUS ink-free band per page, final page excluded because
   a short last page is how a document ends):

       as shipped                              11 pages, page 2 of 11 = 75.8% ink-free
       let .st break, protect its atoms        11 pages, page 2 still 75.8%   <- REFUTED
       un-grid .stations only                  11 pages, page 2 still 75.8%   <- REFUTED
       hide .rail only (keep the grid)         12 pages, TWO 100% blank pages <- WORSE
       un-grid + let .st/.pair break (this)    11 pages, worst mid-band 48.4%, none >=60%

   The isolation matters because the first two hypotheses were wrong and the third made
   it worse. `.stations` is a two-column GRID (224px rail | stations) and Chromium
   fragments a grid ROW as a unit, so the rail's row reserved the whole of page 2 for a
   first station that could not fit beside it. Un-gridding alone does not help — the
   station is still an unbreakable ~1010px atom — and hiding the rail alone leaves an
   empty 224px column that pushes TWO pages fully blank. Both halves are needed.

   `.pair` is un-gridded for the same reason and its 104px stagger zeroed: the stagger
   exists to give 1.4 and 1.5 distinct SCROLL bands for the outline rail, and there is
   no scrolling on paper.

   Content is preserved exactly — pdftotext word-multiset diff against the previous
   render shows nothing missing and all 8 stations present. The tighter alternative
   (also hiding the rail) reaches 10 pages / 36.1%, but costs the 28-word "ON THIS PAGE"
   outline, which is a useful table of contents in a document meant to be circulated.
   That is a page-owner judgment, so it is NOT taken here. */
@media print{
  #stations,#closed-loop,.stations,.stack{break-inside:auto;page-break-inside:auto}
  /* un-grid: a grid row is fragmented as a unit and both of these are two-column */
  .stations,.pair{display:block}
  .rail{position:static;break-inside:avoid;margin-bottom:14pt}
  /* the stagger buys 1.5 its own SCROLL band in the rail; paper does not scroll */
  .pair > *:nth-child(2){margin-top:0}
  /* a .st--media with its figure is up to 1010px against ~1085px of A4 content height.
     That is not an atom, it is most of a page, so it must be allowed to break. */
  .st,.pair{break-inside:auto;page-break-inside:auto}
  .st h3,.pair .st h3{break-after:avoid;page-break-after:avoid}
  .st .lede,.caps li,.verb{break-inside:avoid;page-break-inside:avoid}
  .loop li,.verdict,.hook,.answer,.pull,figure{break-inside:avoid;page-break-inside:avoid}
  /* the ring and the ledger are one argument; do not let a page break land
     between them. `.loopgrid` is a two-column grid and Chromium fragments a grid
     ROW as a unit — the same trap this file documents for `.stations` — so it is
     un-gridded for print and the two stack. */
  .loopgrid{display:block}
  .ret-m{display:none}
  .st--callout .body{border:1px solid rgba(16,24,32,.28);background:#fff}
  .contact a{color:#101820}
}

/* ══ STATION RAIL — replaces the axonometric ═══════════════════════════════════
   The drawing needed 409 words to say what it was not (not a flow diagram, not a
   measurement, numbering ours). Photographs of the eight stations need none of that,
   because a photograph of a boiler does not imply a pipework claim. The two real
   disclosures inside those 409 words survive in .rail-foot below; the rest went with
   the thing that required them.
   POSITION indicator, not a flow diagram: it says which of eight you are looking at. */
.srail{position:relative;height:600vh;margin:0 0 var(--d5)}
.srail .stick{position:sticky;top:0;height:100vh;overflow:hidden;display:flex;flex-direction:column;
  background:var(--mill-navy)}
.srail .bar{display:flex;align-items:center;border-bottom:1px solid var(--hairline);flex:none;
  font:600 12px/1 var(--f-mono);letter-spacing:.17em;text-transform:uppercase}
.srail .bar>div{padding:15px 20px;border-right:1px solid var(--hairline);color:var(--ink-ghost);white-space:nowrap}
.srail .bar b{color:var(--airei-blue-lift);font-variant-numeric:tabular-nums;margin-left:10px}
.srail .bar .nm{color:var(--ink);letter-spacing:.11em;min-width:0;flex:1 1 auto;overflow:hidden}
.srail .bar .nm-v{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
.srail .track{flex:1;display:flex;align-items:center;padding:26px 0 26px 5vw;will-change:transform}
.scard{flex:0 0 clamp(268px,25vw,410px);margin-right:clamp(18px,2.2vw,34px);display:flex;
  flex-direction:column;opacity:.26;transform:translateY(14px) scale(.97);
  transition:opacity .5s var(--e-quad),transform .5s var(--e-quart)}
.scard.live{opacity:1;transform:none}
.scard .shot{position:relative;aspect-ratio:4/5;overflow:hidden;background:var(--device-recess);border:1px solid var(--hairline)}
.scard .shot img{width:100%;height:100%;object-fit:cover;display:block;transform:scale(1.06);
  transition:transform 1.3s var(--e-quart);filter:saturate(.88) contrast(1.04)}
.scard.live .shot img{transform:scale(1)}
.scard .shot::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to top,rgba(11,21,32,.94) 0%,rgba(11,21,32,.24) 42%,rgba(11,21,32,0) 66%)}
.scard .cap{position:absolute;left:17px;right:17px;bottom:15px;z-index:2}
.scard .cap .top{display:flex;align-items:center;gap:10px;margin-bottom:7px}
.scard .ic{width:19px;height:19px;color:var(--airei-blue-lift);flex:none}
.scard .num{font:600 12px/1 var(--f-mono);letter-spacing:.14em;color:var(--ink-ghost)}
.scard h3{margin:0;font-size:clamp(17px,1.5vw,23px);line-height:1.08;letter-spacing:-0.0041em;font-weight:600;color:var(--ink)}
.scard .mini{margin-top:14px;min-height:108px}
.scard .mini ul{list-style:none;margin:0;padding:0}
.scard .mini li{position:relative;padding-left:18px;margin-bottom:6px;font-size:13.5px;line-height:1.5;color:var(--ink-muted)}
.scard .mini li::before{content:"";position:absolute;left:0;top:.62em;width:8px;height:1px;background:var(--crude-amber)}
.scard .mon{display:inline-block;font:600 11px/1 var(--f-mono);letter-spacing:.11em;text-transform:uppercase;
  color:var(--airei-blue-lift);border:1px solid rgba(46,155,255,.34);padding:5px 9px;margin-bottom:10px}
.srail .steps{flex:none;border-top:1px solid var(--hairline);background:rgba(11,21,32,.74);
  padding:16px 26px 20px;position:relative}
.srail .steps .line{position:absolute;left:26px;right:26px;top:30px;height:1px;background:var(--hairline)}
/* scaleX, not width: width animates layout on every frame, and the house rule allows
   transform for exactly this reason. transform-origin pins the growth to the left edge
   so the fill still reads as a progress bar rather than a bar that grows both ways. */
.srail .steps .line i{position:absolute;inset:0 auto 0 0;width:100%;transform:scaleX(0);
  transform-origin:left center;background:var(--airei-blue);transition:transform .25s linear}
.srail .nodes{position:relative;display:flex;justify-content:space-between;gap:6px}
.snode{display:flex;flex-direction:column;align-items:center;gap:9px;flex:1;min-width:0;
  background:0;border:0;padding:0;cursor:pointer;color:inherit;font:inherit}
.snode .dot{width:28px;height:28px;border-radius:50%;border:1px solid var(--mill-steel);background:var(--panel-navy);
  display:grid;place-items:center;color:var(--ink-ghost);flex:none;
  transition:border-color .3s var(--e-quart),background .3s var(--e-quart),
             color .3s var(--e-quart),transform .3s var(--e-quart)}
/* box-shadow is deliberately NOT transitioned: the house list allows transform, opacity and
   colour because those composite; box-shadow repaints. The active node's glow ring still
   appears, it just appears at once rather than fading in — which is what a position
   indicator wants anyway. */
.snode .dot svg{width:15px;height:15px}
.snode.done .dot{border-color:rgba(46,155,255,.5);color:var(--airei-blue-lift)}
.snode.on .dot{border-color:var(--airei-blue-lift);color:var(--mill-navy);background:var(--airei-blue-lift);
  transform:scale(1.18);box-shadow:0 0 0 5px rgba(46,155,255,.18)}
.snode>span:last-child{font:600 10.5px/1.25 var(--f-mono);letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-ghost);text-align:center;transition:color .3s;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;max-width:100%}
.snode.on>span:last-child{color:var(--ink)}
.snode:focus-visible{outline:2px solid var(--signal-gold);outline-offset:3px}
.rail-foot{max-width:var(--measure);margin:var(--d3) auto 0}
/* NO-JS + reduced-motion: the rail degrades to a plain scrolling row of the same eight
   cards. A sticky 600vh section with no transform is 6 blank screens, so the height and
   the stick BOTH have to go, not just the animation. */
/* The degraded rail STACKS, it does not scroll sideways.
   The first version kept the horizontal track and gave it overflow-x:auto. The ink-overflow
   test measured 192 text runs sitting outside the viewport at 1100-1920px with JS off, and it
   was right to: a horizontal scroller hides seven of eight stations behind a gesture the page
   never advertises, and with no JavaScript there is no step rail to reveal that they exist.
   A vertical stack has none of that problem — every station is reached by the scroll the
   reader is already doing. */
.srail.noscript,.srail.static{height:auto;margin-bottom:var(--d4)}
.srail.noscript .stick,.srail.static .stick{position:static;height:auto;overflow:visible}
.srail.noscript .track,.srail.static .track{display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(18px,2.4vw,30px);
  overflow:visible;padding:26px 0;transform:none!important}
.srail.noscript .scard,.srail.static .scard{opacity:1;transform:none;flex-basis:auto;
  max-width:none;margin-right:0}
.srail.noscript .steps,.srail.static .steps{display:none}
.srail.noscript .bar,.srail.static .bar{display:none}
.srail.noscript .scard,.srail.static .scard{transition:none}
.srail.noscript .scard .shot img,.srail.static .scard .shot img{transition:none;transform:none}
@media (prefers-reduced-motion:reduce){
  .srail{height:auto;margin-bottom:var(--d4)}
  .srail .stick{position:static;height:auto;overflow:visible}
  .srail .track{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
    gap:clamp(18px,2.4vw,30px);overflow:visible;padding:26px 0;transform:none!important}
  .scard{opacity:1;transform:none;flex-basis:auto;max-width:none;margin-right:0;transition:none}
  .scard .shot img{transform:none;transition:none}
  .srail .steps,.srail .bar{display:none}
}
@media (max-width:900px){
  .snode>span:last-child{display:none}
  .srail .steps .line{top:26px}
  .srail .bar>div:last-child{display:none}
  .srail .track{padding-left:4vw}
}

/* FULL-BLEED. The rail sits inside .wrap, whose max-width made .stick 1112px at a
   1440 viewport — while the JS measured against innerWidth (1440). A card at rail-x
   576 actually sat at viewport-x 740, so the "which card is live" test and the travel
   maths were in two different coordinate spaces and station 8 was unreachable. Break
   the rail out of the wrap AND measure in the track's own space (station-rail.js).
   100vw + the negative half-margin is the standard escape; the calc keeps it centred
   whatever --wrap is, and 100vw includes the scrollbar so overflow-x is clipped by
   .stick's own overflow:hidden. */
.srail{width:100vw;max-width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}
.rail-foot{padding:0 var(--gut)}

/* The rail's own bar (station N / 8 + the station name) was rendering at viewport y=0..43
   — underneath header.site, which is sticky at 0..84 with z-index 100. The counter and the
   name, which are the entire point of the bar, were invisible at every viewport. Park the
   stick below the header using the SAME --hdr ladder shell.css declares (65 / 80 / 84), and
   shorten the stick by the same amount so the step nodes still land on the fold. */
.srail .stick{top:var(--hdr-real,var(--hdr));height:calc(100svh - var(--hdr-real,var(--hdr)))}
/* svh, not vh: on mobile Safari/Chrome vh is the LARGEST viewport (toolbars retracted), so
   100vh minus a header still overflows the visible area while the toolbar is showing and
   the step nodes sit below the fold. svh is the smallest, which is the one that is always
   actually visible. */

/* THE CARD IS SIZED BY HEIGHT, AND ITS WIDTH FOLLOWS.
   Sized off viewport WIDTH, a 4:5 card is 450px tall at 1440 and the column (bar + track
   padding + image + bullets + step nodes) overflowed the stick by 67px — the step rail,
   the one control that says where you are, fell below the fold at the desktop sizes most
   visitors use.

   The first repair was algebra: cap the width at 0.8*stickHeight - 242, the -242 standing
   in for bar + padding + bullets + nodes. Measured across a height ladder it was better but
   still WRONG — at 1440x600 the last capability bullet was still clipped by 22px and at
   1280x560 by 42px, because a bullet block's real height is whatever three lines of wrapped
   text happen to need, which no constant can predict.

   So stop predicting it. Give the bullets their natural height, let the photograph take
   whatever vertical space is left, and let 4:5 compute the WIDTH from that. Nothing is
   estimated, so nothing can be estimated wrong.
   Attempt 2 (height drives, width follows) killed the clipping but broke the SHAPE: with
   flex:1 the photograph grew past 4:5 wherever the viewport was tall (0.583 at 1920x1080,
   0.413 at 390x844), and the card collapsed to 160px wide on a phone.

   Attempt 3, shipped: keep the width-driven card, keep the two competing budgets — but
   MEASURE the chrome instead of guessing it. station-rail.js publishes --srail-chrome as
   the real summed height of the bar, the track padding, the tallest bullet block and the
   step nodes, re-measured on resize. The constant that could be wrong is gone; the shape
   is exact at every size because 4:5 is never overridden.

   min-height:0 is required because a flex child will not shrink below its content without it.
   Re-measured 2026-08-27, 1440px wide, viewport height swept 700-1200px, this sheet against a
   comment-closed-early mutant of itself: the rule changes geometry only between 721px and
   739px, where .track renders 1.3-6.3px taller without it. At 720px and below it is inert —
   the max-height:720px branch further down switches .track to display:grid and sets .steps to
   display:none, so no step node is rendered there to push anywhere. An earlier draft of this
   paragraph claimed the reverse ("the step nodes drop below the fold at every viewport height
   at or under 700px"); that is the one band where the rule provably does nothing, and the
   claim is withdrawn. Do NOT read the narrow band as dead code — a dropped rule is dropped at
   every height, which is how this one went missing before:

   THE SENTENCE ABOVE USED TO CLOSE THE COMMENT, AND THE RULE BELOW WAS NOT REACHING THE
   BROWSER. Closing the comment early left "Attempt 2 … 4:5 is never overridden." as live CSS:
   the parser read it as the prelude of a qualified rule, ran the prelude on into
   `.srail .track`, found it invalid, and DROPPED the whole declaration block — silently, the
   way CSS always does. Measured at 1440x760 with the repaired comment as the control:
     comment closed early  computed min-height: auto · rule absent from the CSSOM
     comment repaired      computed min-height: 0px  · rule present
   The rule COUNT is deliberately not given as an absolute: an earlier note recorded 147/148
   accepted rules and this sheet has grown since, so the absolute rots while the finding does
   not. The invariant to re-test is the DELTA, and it depends on WHERE the terminator lands:
   closing it where it used to close, nine lines above the rule, costs exactly one rule and
   that one rule is this one (2026-08-27: 231 -> 230 flattened CSSOM rules); closing it at the
   top of the comment swallows 47 (231 -> 184). Either way the rule is absent from the CSSOM
   and .track computes min-height:auto, which is the pair to assert. */
.srail .track{min-height:0}
.scard{flex-basis:min(clamp(268px,25vw,410px),
       calc((100svh - var(--hdr-real,var(--hdr)) - var(--srail-chrome,300px)) * 0.8))}

/* SHORT WINDOWS GET THE STATIC RAIL, AND THAT IS A DECISION, NOT A FALLBACK.
   A full-height sticky rail has to fit a bar, a 4:5 photograph, three capability bullets
   and eight step nodes between the header and the fold. Under about 720px of viewport
   height it does not, and every attempt to make it fit trades away something the reader
   needs — measured: the photograph collapses to 89px wide, or the step nodes land 236px
   below the fold. A short window gets the same eight cards as a plain horizontal row it
   can swipe. Nothing is hidden; only the scroll choreography is dropped.
   Height-only query, so a short desktop window is treated like a short window, which is
   what it is. */
@media (max-height:720px){
  .srail{height:auto;margin-bottom:var(--d4)}
  .srail .stick{position:static;height:auto;overflow:visible}
  .srail .track{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
    gap:clamp(18px,2.4vw,30px);overflow:visible;padding:26px 0;transform:none!important}
  .scard{opacity:1;transform:none;flex-basis:auto;max-width:none;margin-right:0;transition:none}
  .scard .shot img{transform:none;transition:none}
  .srail .steps,.srail .bar{display:none}
}

/* The eight step nodes are buttons that jump the rail. At 390px they measured 37x28 —
   under the 32px floor on the short axis, and the axis a thumb actually misses on. The
   label is already hidden below 900px, so the node is just a 28px dot with nothing under
   it; give the button the height back without moving the dot. */
@media (pointer:coarse){
  .snode{min-height:44px;justify-content:center}
  .srail .steps{padding:14px 20px 18px}
}

/* Reduced motion for the rail's own chrome. The rail already falls back to a static row
   under prefers-reduced-motion (see the .static block above), but these three still declared
   transitions that would run inside it. Naming them here is not belt-and-braces: the lint is
   right that a transition with no reduced-motion counterpart is an unanswered question. */
@media (prefers-reduced-motion:reduce){
  .srail .steps .line i{transition:none}
  .snode .dot{transition:none}
  .snode>span:last-child{transition:none}
}

/* On a narrow screen the bar's station name WRAPS rather than truncating.
   Ellipsis looked like the tidy answer and the box fitted — but the ink check measures the
   text run, not the box, and it was right to: "FFB RAMP & WEIGHBRIDGE" was being cut to
   "FFB RAMP & WEIGH…", so the reader lost the second half of the station's name. Two lines
   at 11px costs about 14px of a sticky bar and shows the whole thing. */
@media (max-width:460px){
  .srail .bar>div{white-space:normal;padding:9px 12px}
  .srail .bar .nm-v{white-space:normal;text-overflow:clip;line-height:1.25}
  .srail .bar{align-items:stretch}
}


/* THE STATION GRID DIVIDES EIGHT EVENLY, OR IT DOES NOT SPLIT AT ALL.
   `repeat(auto-fit,minmax(230px,1fr))` fits as many columns as the width allows and has no
   idea there are exactly eight cards. Measured: 7 columns at 1920 (so 7 + 1), 6 at 1680
   (6 + 2), 5 at 1440 (5 + 3) — the operator saw the 5 + 3 and was right. An orphan row of
   one or three reads as a mistake no matter how good the cards are.
   Eight divides by 4, 2 and 1. Those are the only column counts this grid may take. */
@media (min-width:1180px){
  .srail.noscript .track,.srail.static .track{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media (max-width:759px){
  .srail.noscript .track,.srail.static .track{grid-template-columns:minmax(0,1fr)}
}
@media (prefers-reduced-motion:reduce){
  @media (min-width:1180px){ .srail .track{grid-template-columns:repeat(4,minmax(0,1fr))} }
  @media (max-width:759px){ .srail .track{grid-template-columns:minmax(0,1fr)} }
}
@media (max-height:720px){
  @media (min-width:1180px){ .srail .track{grid-template-columns:repeat(4,minmax(0,1fr))} }
  @media (max-width:759px){ .srail .track{grid-template-columns:minmax(0,1fr)} }
}

/* Cards in a row share a top edge. `align-items` was inheriting `center` from the sticky
   track, where it is correct — there the row is one line of cards and centring them on the
   band is the point. In the stacked grid it means eight cards of eight different heights each
   float at their own offset, so a tidy 4 + 4 still reads as ragged. Measured at 1440: tops of
   1302 / 1283 / 1303 / 1307 across one row. */
.srail.noscript .track,.srail.static .track{align-items:start}
@media (prefers-reduced-motion:reduce){ .srail .track{align-items:start} }
@media (max-height:720px){ .srail .track{align-items:start} }

/* SNAPPED RAIL — the phone case, set by station-rail.js when the viewport cannot hold
   a card plus a full pitch of drift (the condition is measured there, not guessed here).
   The transform then lands on whole stations instead of interpolating between them, so
   the station the bar names is always the one at rest and always whole. Without an ease
   that quantised jump reads as a glitch rather than a step, so give it one — short enough
   that a fast scroll still feels driven by the thumb.
   Scoped to .snap: the continuous desktop rail writes a transform every frame and must
   never carry a transition, or every frame would fight the last one. */
.srail.snap .track{transition:transform .26s var(--e-cubic)}
/* The curve above was a hand-written cubic-bezier(.22,.61,.36,1) — outside the three
   approved tokens, which is a hard EASING violation in qa/motion-allow.json. Swapped
   for the nearest token rather than widening the allowlist: sampled at 201 points,
   --e-cubic differs from that bezier by mean |dy| 0.0070 and max 0.0174, so the step
   the note above describes is preserved and the contract holds. --e-quart was 7x
   further (mean 0.0493) and --e-quad 12x (mean 0.0835). */
@media (prefers-reduced-motion:reduce){ .srail.snap .track{transition:none} }

/* The station rail's own index links measured 350x40 at 390 — four pixels under the floor,
   on the list that is the only non-scrolling way into a station. Flex already, so the box
   grows and the label does not move. */
@media (pointer:coarse){ .rail li a{min-height:44px;display:flex;align-items:center} }

/* ── .witness — two portrait delegate clips ──────────────────────────────────────
   These are 9:16, shot on a phone held upright. .filmplayer is hard-set to
   aspect-ratio:16/9 with object-fit:cover, and a 9:16 source in that box crops a
   standing man to a band across his chest — the badge that identifies him, and the
   backdrop that places him at the mill, both leave the frame. So the portrait case
   carries its own ratio and switches to contain.

   The width is CAPPED, not fluid. The source is a handheld phone clip; rendered
   700px wide it reads as a production mistake rather than as what it is — someone's
   phone, at the plant, on the day. 340px is about the width the footage was framed
   for. Below 760px the grid collapses to one column on its own. */
/* Spacing is written as a literal clamp, matching .pair and .st--media above. There is
   NO spacing scale in tokens.css — --d1..--d6 are DURATIONS (--d3 is 320ms). Writing
   `gap:var(--d3)` parses as `gap:320ms`, which is invalid, and an invalid value drops
   the WHOLE declaration: the two players then sit flush against each other and read as
   one broken image. That is not hypothetical — it is what this rule did on its first
   render, and it is worth knowing that `.filmplayer` in base.css carries the same
   mistake today (`margin:var(--d3) 0 0`), which is why the film has no top margin. */
.witness{display:grid;gap:clamp(24px,3.4vw,44px);
  grid-template-columns:repeat(auto-fit,minmax(260px,320px));
  justify-content:start;margin-top:clamp(26px,3vw,40px)}
.filmplayer.portrait{aspect-ratio:9/16;width:100%;max-width:340px;margin-top:0}
.filmplayer.portrait video{object-fit:contain}
