/* ==========================================================================
   Selective Amplifiers — main.css
   Design system.

   Direction, and where it comes from:

   The SD125.4's faceplate is composed of two 0.25 mm hairlines that run across
   the panel and BREAK around the status legend and LED windows — they frame
   the model number and the wordmark rather than striking through them. The
   supplied logo is built the same way: word, hairline, word. That device — a
   rule that opens to make room for a label — is this site's structural
   signature, and it is the one place the design raises its voice. Everything
   around it stays quiet: warm off-white ground, near-black ink, Raleway only,
   hairline borders, 2 px radii (the real panel is R1.5 on a 205.7 mm face —
   hardware corners are tight, so ours are too), no gradients, no shadows.

   Colour is load-bearing in exactly one place: the indicator tokens, which
   carry LED state. They are never used as decoration.

   Section order below is dependency order — tokens, base, layout, then
   components. Components are single-class selectors; nothing here should need
   to out-specify anything else.
   ========================================================================== */

/* ── 1. Tokens ─────────────────────────────────────────────────────────── */

:root{
  /* ground */
  --ink:#faf9f7;              /* warm off-white page ground */
  /* The same colour as channels, for the one place that needs it with alpha:
     the home hero's scrim over a photograph. `color-mix()` would be the
     tokened way and is what the masthead uses, but the masthead can fall back
     to fully opaque and a scrim cannot — opaque means the photograph is gone.
     Keep these three numbers in step with --ink above. */
  --ink-rgb:250,249,247;
  --panel:#eeece7;            /* raised warm surface */
  --paper:#161616;            /* near-black ink */
  /* --dim was #6e6e6e on the placeholder. That is 4.85:1 on --ink but only
     4.32:1 on --panel, and card body copy sits on --panel — so it fails AA
     exactly where it is used most. #666 gives 5.46:1 and 4.86:1. */
  --dim:#666666;              /* secondary text */
  --line:rgba(20,20,20,0.08); /* hairline, quiet */
  --rule:rgba(20,20,20,0.35); /* hairline, deliberate */

  /* indicators — LED state only, never decoration */
  --signal:#2f7d52;
  --clip:#b8452f;
  --standby:#8d8d8d;

  /* type */
  --font:'Raleway', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --track:.18em;              /* uppercase micro-label tracking */
  --track-wide:.26em;

  --fs-micro:.7rem;
  --fs-small:.8rem;
  --fs-body:clamp(.95rem, .2vw + .9rem, 1.02rem);
  --fs-lede:clamp(1.02rem, .7vw + .88rem, 1.28rem);
  --fs-h3:clamp(1.02rem, .4vw + .94rem, 1.18rem);
  --fs-h2:clamp(1.45rem, 1.7vw + 1.05rem, 2.1rem);
  --fs-h1:clamp(1.95rem, 3.4vw + 1.1rem, 3.35rem);
  --fs-display:clamp(2.3rem, 5vw + 1rem, 4.4rem);

  /* space */
  --s1:.375rem;
  --s2:.625rem;
  --s3:1rem;
  --s4:1.5rem;
  --s5:2.25rem;
  --s6:3.5rem;
  --s7:5rem;

  --gutter:clamp(20px, 5vw, 64px);
  --sec-pad:clamp(44px, 7vw, 96px);
  /* Space above the eyebrow that opens a page. Shared by .page-head and the
     home .hero so every page's first line starts at the same height — they
     drifted apart once (the hero was 48/9vw/120px against 46/8vw/104px, which
     put the home page's eyebrow up to 16px lower than every other page's) and
     one token is what stops that recurring. Only the space BELOW them differs:
     the hero has an object under its buttons and .page-head does not. */
  --head-pad-top:clamp(46px, 8vw, 104px);
  --measure:64ch;
  --maxw:1180px;

  /* shape */
  --r:2px;                    /* panel corner, scaled down */
  --r-pill:100px;             /* indicators only */

  /* The EXPANDED masthead's height, derived rather than measured, so no JS is
     involved and it cannot drift. Nothing sets that height directly: the bar
     is the wordmark plus 20px of padding either side plus the 1px hairline,
     and the wordmark is clamp(146px,19vw,200px) wide at 1820:747 — so its
     height is that clamp over 2.4364. (§11's min-height is a floor of 88/72px
     and never binds: the smallest the mark goes is 146px ⇒ 60+41 = 101px.)
     Verified against getBoundingClientRect: 123.08px @1440, 100.92px @390.
     Change the mark's width clamp or its padding and this must follow.
     Used by the home hero to run its photograph up behind the header. */
  --masthead-h:calc(clamp(146px, 19vw, 200px) / 2.4364 + 41px);

  /* motion */
  --t:.2s ease;
  --t-reveal:.45s cubic-bezier(.22,.7,.3,1);
}

/* ── 2. Base ───────────────────────────────────────────────────────────── */

*,*::before,*::after{box-sizing:border-box;}

html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;}

body{
  margin:0;
  background:var(--ink);
  color:var(--paper);
  font-family:var(--font);
  font-size:var(--fs-body);
  font-weight:400;
  line-height:1.68;
  min-height:100vh;
  display:flex;
  flex-direction:column;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}

img,svg{max-width:100%;height:auto;}

a{color:var(--paper);text-decoration:none;}

h1,h2,h3,h4{margin:0 0 var(--s3);line-height:1.16;text-wrap:balance;}
h1{font-size:var(--fs-h1);font-weight:300;letter-spacing:-.015em;}
h2{font-size:var(--fs-h2);font-weight:300;letter-spacing:-.01em;}
h3{font-size:var(--fs-h3);font-weight:500;letter-spacing:0;}
h4{font-size:var(--fs-small);font-weight:600;letter-spacing:.02em;}

p{margin:0 0 var(--s3);}
p:last-child{margin-bottom:0;}

ul,ol{margin:0 0 var(--s3);padding-left:1.3em;}
li{margin-bottom:var(--s1);}

strong{font-weight:600;}
em{font-style:italic;}

/* Technical strings — model numbers in running copy, settings names, tokens.
   System monospace stack only: no second webfont is loaded, per the brief. */
code{
  padding:.1em .35em;
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r);
  font-family:ui-monospace,'SF Mono',Menlo,Consolas,monospace;
  font-size:.86em;
  letter-spacing:0;
}

hr{
  height:0;
  margin:var(--s5) 0;
  border:0;
  border-top:1px solid var(--line);
}

/* Numbers are hardware data here: keep them lining and column-aligned. */
table,.tnum{font-variant-numeric:tabular-nums lining-nums;}

:focus-visible{outline:2px solid var(--paper);outline-offset:3px;}

::selection{background:var(--paper);color:var(--ink);}

.skip-link{
  position:absolute;
  left:-9999px;
  top:0;
  z-index:20;
  background:var(--ink);
  padding:12px 20px;
  border:1px solid var(--rule);
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
}
.skip-link:focus{left:var(--gutter);}

/* ── 3. Type utilities ─────────────────────────────────────────────────── */

/* The utility register: panel silkscreen. Small, uppercase, widely tracked. */
.label{
  display:block;
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  line-height:1.4;
}

.eyebrow{
  margin:0 0 var(--s4);
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
}

.lede{
  font-size:var(--fs-lede);
  font-weight:300;
  line-height:1.6;
  color:var(--dim);
  max-width:var(--measure);
}

.display{
  font-size:var(--fs-display);
  font-weight:300;
  line-height:1.05;
  letter-spacing:-.025em;
}

.measure{max-width:var(--measure);}
.dim{color:var(--dim);}

.prose > * + *{margin-top:var(--s3);}
.prose p,.prose ul,.prose ol{max-width:var(--measure);}
.prose h2{margin-top:var(--s5);}
.prose h3{margin-top:var(--s4);}
.prose a{border-bottom:1px solid var(--rule);transition:border-color var(--t);}
.prose a:hover{border-bottom-color:var(--paper);}

/* ── 4. Layout ─────────────────────────────────────────────────────────── */

main{flex:1;}

.container{
  width:100%;
  max-width:var(--maxw);
  margin:0 auto;
  padding:0 var(--gutter);
}

.sec{padding:var(--sec-pad) 0;}
.sec + .sec{padding-top:0;}
.sec-panel{
  background:var(--panel);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
}
/* A panel band paints a background, so it keeps its own padding on both sides
   and gives it back to whatever follows. Must come after `.sec + .sec` — equal
   specificity, later wins. */
.sec + .sec-panel,
.sec-panel + .sec{padding-top:var(--sec-pad);}
.sec-panel .card,
.sec-panel .notice{background:var(--ink);}

/* ── Dark band ────────────────────────────────────────────────────────────
   ONE inverted section per page, to give a long light page a spine and to make
   the near-black faceplate read as part of a system rather than as the only
   dark thing on the site. #161616 is not a new colour: it is the panel's own
   Pantone 100% Black flood, the same value .fp-panel fills with.

   Where it goes: the Power Share section on every page that has one (home,
   technology, SD125.4) — the product's signature feature getting the signature
   treatment, and the tracks read as an instrument on black. Support has no
   Power Share section, so its band is the commissioning sequence. Two bands on
   one page reads as stripes; don't.

   A band wants a SHORT section. Technology's signal path is that page's most
   distinctive component and was tried first, but at 883 words and 24% of the
   page it put far too long a run of reversed body copy in front of the reader.
   Power Share is 311 words and 9%. Measure before moving a band.

   It re-maps TOKENS rather than restyling components. Everything inside — the
   hairlines, the prose, the Power Share tracks — is already token-driven, so
   the whole subtree inverts with no per-component rules. That is the argument
   for the token discipline in §1 paying rent.

   The values are measured, not eyeballed (notes/contrast-audit.js):
     --paper #f2f1ef on #161616 = 16.0:1
     --dim   #a8a8a8 on #161616 =  7.6:1   (light-mode #666 would be 2.4:1)
   --dim CANNOT carry over from light mode; that is the whole trap here.

   ⚠️ One component must never go in a band: the FACEPLATE. .fp-panel fills
   with #161616, the exact value of the band, so a faceplate here loses its
   edges and dissolves into the background. That rules out the SD125.4 page's
   front-panel section, which is otherwise a tempting candidate.

   Checked and safe: prose, .sec-head, .split, .powershare, .datum, .card,
   .notice, .pill (its text is --dim at 7.6:1; its dot is a graphic, and the
   indicator tokens clear the 3:1 that graphics need — --signal is 3.6:1,
   --standby 5.5:1). Cards and notices sat on --ink in a light band and would
   vanish here, so they take --panel; see below. */
.sec-dark{
  --ink:#161616;                    /* the faceplate's own flood black */
  --panel:#202020;                  /* raised surface, one step off the ground */
  --paper:#f2f1ef;
  --dim:#a8a8a8;
  --line:rgba(255,255,255,.10);
  --rule:rgba(255,255,255,.34);
  background:var(--ink);
  color:var(--paper);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
}
/* Same give-back rule as .sec-panel: a band paints a background, so it keeps
   its padding on both sides. */
.sec + .sec-dark,
.sec-dark + .sec{padding-top:var(--sec-pad);}
/* Surfaces that sat on --ink in a light band need the raised tone here, or
   they disappear into the band they are supposed to sit on. */
.sec-dark .card,
.sec-dark .notice,
.sec-dark .ps-track{background:var(--panel);}

/* Two-column: text and figure. Collapses early — the figures are wide. */
/* minmax(0,1fr), not 1fr: a plain `1fr` track takes its minimum from the
   content's min-content width, so one .table-scroll child — whose table carries
   a 560 px minimum by design (§9) — dragged the whole collapsed column to
   560 px and pushed the page sideways at 390 px. The desktop rule below already
   guards against this; the single-column rule did not. Latent for any .split
   holding a wide table. */
.split{
  display:grid;
  gap:var(--s5) var(--s6);
  grid-template-columns:minmax(0,1fr);
}
@media (min-width:900px){
  .split{grid-template-columns:minmax(0,7fr) minmax(0,5fr);align-items:start;}
  .split-wide{grid-template-columns:minmax(0,5fr) minmax(0,7fr);}
}

.grid{
  display:grid;
  gap:var(--s4);
  grid-template-columns:repeat(auto-fit, minmax(min(240px,100%), 1fr));
}
.grid-2{grid-template-columns:repeat(auto-fit, minmax(min(300px,100%), 1fr));}

.stack > * + *{margin-top:var(--s3);}
.stack-lg > * + *{margin-top:var(--s5);}

/* rack-ear screws, page corners — carried from the placeholder. Absolute, not
   fixed: they belong to the top of the page like the ears on a rack unit, and
   pinning them to the viewport left two dots hovering over the content. */
.rack-screw{
  position:absolute;
  top:18px;
  width:9px;
  height:9px;
  border-radius:50%;
  background:var(--panel);
  box-shadow:inset 0 0 0 1px var(--line), 0 1px 0 rgba(20,20,20,.15);
  pointer-events:none;
  z-index:1;
}
.rack-screw.left{left:18px;}
.rack-screw.right{right:18px;}
@media (max-width:600px){.rack-screw{display:none;}}

/* ── 5. Section header — the breaking hairline ─────────────────────────── */
/* The signature. A hairline crosses the full measure and opens around the
   label, exactly as the faceplate's two hairlines open around the LED stack.
   Short segment left, label, long segment right — the panel's own asymmetry. */

.sec-head{
  display:flex;
  align-items:center;
  gap:var(--s3);
  margin-bottom:var(--s4);
}
.sec-head::before,
.sec-head::after{
  content:'';
  height:1px;
  background:var(--rule);
  flex:0 0 auto;
}
.sec-head::before{width:clamp(18px,3vw,34px);}
.sec-head::after{flex:1 1 auto;width:auto;}

/* The label is an `h2.label` wherever it is the section's only heading, and a
   plain `span.label` where the section body carries its own `<h2>`.
   `margin:0` is what makes the two interchangeable:
   `.label` already overrides the type, but not the heading's bottom margin,
   which would push the label off the hairline it is supposed to sit in. */
.sec-head .label{color:var(--paper);white-space:nowrap;margin:0;}
.sec-head::after{order:1;}

/* Trailing datum, e.g. a spec reference or a count. Drops out when tight. */
.sec-head-meta{
  order:2;
  flex:0 0 auto;
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
}
@media (max-width:520px){
  .sec-head-meta{display:none;}
}

/* Standalone divider, for a subsection break that carries no label.
 *
 * It is UNBROKEN, and that is the point. The gap in `.sec-head` above is not a
 * decorative notch — it is the space a label occupies, the same way the
 * faceplate's hairlines open around the LED legend. Copy that break to a rule
 * with nothing in it and the reader sees a gap with no reason to exist, which
 * is exactly how it read on the technology page above "Why this is not
 * bridging". So: a label means a break, no label means a continuous line, and
 * the signature stays meaningful rather than becoming a texture.
 *
 * Keeps the class (rather than deferring to `hr`) because the vertical rhythm
 * is part of the system and every subsection break should share it. */
.rule-break{
  height:1px;
  margin:var(--s5) 0;
  background:var(--rule);
}

/* ── 6. Buttons ────────────────────────────────────────────────────────── */
/* Rectangular, 2 px radius: actions are hardware. Pills are reserved for
   state readouts (§7) so the two are never confused. */

.btn{
  display:inline-flex;
  align-items:center;
  gap:var(--s2);
  padding:13px 26px;
  border:1px solid var(--rule);
  border-radius:var(--r);
  background:none;
  color:var(--paper);
  font-family:var(--font);
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  line-height:1;
  cursor:pointer;
  transition:background var(--t), color var(--t), border-color var(--t);
}
.btn:hover{background:var(--paper);color:var(--ink);border-color:var(--paper);}

.btn-solid{background:var(--paper);color:var(--ink);border-color:var(--paper);}
.btn-solid:hover{background:none;color:var(--paper);border-color:var(--paper);}

.btn-quiet{border-color:var(--line);color:var(--dim);}
.btn-quiet:hover{background:none;color:var(--paper);border-color:var(--rule);}

.btn[disabled],.btn[aria-disabled="true"]{
  opacity:.45;
  pointer-events:none;
}

.btn-row{display:flex;flex-wrap:wrap;gap:var(--s2) var(--s3);align-items:center;}

/* Text link that behaves like an action without becoming a button. */
.link-arrow{
  display:inline-flex;
  align-items:baseline;
  gap:.5em;
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  border-bottom:1px solid var(--line);
  padding-bottom:3px;
  transition:border-color var(--t);
}
.link-arrow::after{content:'\2192';transition:transform var(--t);}
.link-arrow:hover{border-bottom-color:var(--paper);}
.link-arrow:hover::after{transform:translateX(3px);}

/* ── 7. Indicators ─────────────────────────────────────────────────────── */
/* Pill = a state readout, borrowed from the panel's LED legend. Not a button,
   never clickable. Colour here is meaning, not styling. */

.pill{
  display:inline-flex;
  align-items:center;
  gap:9px;
  padding:8px 18px;
  border:1px solid var(--line);
  border-radius:var(--r-pill);
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  line-height:1.3;
}
.pill::before{
  content:'';
  flex:0 0 auto;
  width:6px;
  height:6px;
  border-radius:50%;
  background:var(--standby);
}
.pill-signal::before{background:var(--signal);}
.pill-clip::before{background:var(--clip);}
.pill-live::before{
  background:var(--signal);
  animation:led 2.4s ease-in-out infinite;
}
@keyframes led{
  0%,100%{opacity:.4;}
  50%{opacity:1;}
}

/* Inline legend row, as printed on the panel. */
.legend{
  display:flex;
  flex-wrap:wrap;
  gap:var(--s2) var(--s4);
  list-style:none;
  margin:0;
  padding:0;
}
.legend li{
  display:flex;
  align-items:center;
  gap:8px;
  margin:0;
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
}
.legend li::before{
  content:'';
  width:5px;height:5px;border-radius:50%;
  background:var(--rule);
}

/* ── 8. Cards ──────────────────────────────────────────────────────────── */

.card{
  display:flex;
  flex-direction:column;
  gap:var(--s2);
  padding:var(--s4);
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r);
  transition:border-color var(--t);
}
.card h3{margin:0;}
.card p{margin:0;color:var(--dim);font-size:var(--fs-small);line-height:1.6;}
.card .label{margin-bottom:var(--s1);}
.card-link:hover{border-color:var(--rule);}
.card .link-arrow{align-self:flex-start;}
/* Same reason as .link-arrow above: .card is a stretch-aligned flex column, so
   an inline-ish child would otherwise span the full card width. A pill is a
   state readout (§7) and has to be the width of its own text or it stops
   reading as a badge. Used by the support page's documentation index. */
.card .pill{align-self:flex-start;}

/* Card media. The image bleeds to the card's edges, so it cancels
   the card's own padding and sits above everything else in the flex column.
   A fixed 4:3 is baked into the FILES (800 × 600) rather than enforced with
   object-fit, so the intrinsic width/height attributes are honest, nothing is
   cropped twice, and a card row lines up without a CSS aspect-ratio rule. */
.card-media{
  display:block;
  width:calc(100% + var(--s4) * 2);
  /* §2's reset gives every img `max-width:100%`, which caps this back to the
     CONTENT box and hands the bleed calculation to the negative margin alone —
     the image ends up card-width-minus-padding and shunted left, with a gap
     down the right side of every card. Measured, not guessed: 195 px inside a
     245 px card. */
  max-width:none;
  margin:calc(var(--s4) * -1) calc(var(--s4) * -1) var(--s2);
  border-radius:var(--r) var(--r) 0 0;
  /* The card's own border already draws the outside edge. */
  border-bottom:1px solid var(--line);
}

/* Figure-and-caption for the viz/ partials and any imagery. */
.figure{margin:0;}
.figure img{display:block;width:100%;border:1px solid var(--line);border-radius:var(--r);}

/* A full-width environmental band. The image files are cropped 8:3,
   which is shallow enough to sit between two blocks of text without shoving
   the next section off the screen on a phone. */
.band{margin:0;}
.band img{
  display:block;
  width:100%;
  height:auto;
  border:1px solid var(--line);
  border-radius:var(--r);
}
.figure figcaption{
  margin-top:var(--s2);
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
}

/* A single number, presented as an instrument reading. */
.datum{display:flex;flex-direction:column;gap:6px;}
.datum-value{
  font-size:clamp(1.7rem,3vw,2.5rem);
  font-weight:300;
  line-height:1;
  letter-spacing:-.02em;
  font-variant-numeric:tabular-nums lining-nums;
}
.datum-value small{font-size:.42em;font-weight:500;letter-spacing:.04em;margin-left:.35em;color:var(--dim);}

/* ── 9. Spec table ─────────────────────────────────────────────────────── */
/* The most-repeated component on the site. Rows are hairlines; the label
   column is silkscreen, the value column is data. */

.specs{
  width:100%;
  border-collapse:collapse;
  text-align:left;
}
.specs caption{
  padding-bottom:var(--s3);
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  text-align:left;
}
.specs th,
.specs td{
  padding:14px var(--s3) 14px 0;
  border-bottom:1px solid var(--line);
  vertical-align:baseline;
}
.specs th[scope="row"]{width:34%;}
.specs th{
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  white-space:normal;
}
.specs td{font-size:var(--fs-small);line-height:1.55;}
.specs td:last-child,.specs th:last-child{padding-right:0;}
.specs tr:last-child th,.specs tr:last-child td{border-bottom:0;}
.specs-note{
  margin-top:var(--s3);
  font-size:var(--fs-micro);
  color:var(--dim);
  letter-spacing:.03em;
}

@media (max-width:600px){
  .specs:not(.specs-cols) th,
  .specs:not(.specs-cols) td{display:block;width:auto;padding-right:0;}
  .specs:not(.specs-cols) th{border-bottom:0;padding-bottom:4px;}
  .specs:not(.specs-cols) td{padding-top:0;}
}

/* Comparison table — many columns, horizontally scrollable rather than
   crushed. The wrapper does the scrolling so the page never does. */
/* Cable gauge tables. Was `.tech-cable` in technology.css until the client's
   first review moved the tables to the documentation page — "would move this to
   SUPPORT/DOCUMENTATION tab" — which is a page that loads no extra stylesheet,
   so the rules belong here now. Renamed with them: nothing about them was ever
   specific to the technology page. */
.cable-tables{
  display:grid;
  gap:var(--s5) var(--s4);
  grid-template-columns:repeat(auto-fit, minmax(min(300px,100%), 1fr));
}
.cable-tables .table-scroll > table{min-width:0;}
/* Four short numeric columns: the 34% label column a two-column spec table gets
   above is far too wide here, and the first column is the narrowest. */
.cable-tables .specs th[scope="row"]{width:auto;}
.cable-tables .specs th,
.cable-tables .specs td{padding-right:var(--s2);}
.cable-tables .specs td{font-variant-numeric:tabular-nums lining-nums;}

.table-scroll{
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
}
.table-scroll > table{min-width:560px;}

/* ── 10. Form controls ─────────────────────────────────────────────────── */

.field{display:flex;flex-direction:column;gap:8px;margin-bottom:var(--s4);}
.field-row{display:grid;gap:var(--s3);grid-template-columns:1fr;}
@media (min-width:640px){.field-row{grid-template-columns:1fr 1fr;}}

.field > .label{color:var(--paper);}
.field-hint{font-size:var(--fs-micro);color:var(--dim);letter-spacing:.03em;}

.input,
.textarea,
.select{
  width:100%;
  padding:12px 14px;
  background:var(--ink);
  border:1px solid var(--rule);
  border-radius:var(--r);
  color:var(--paper);
  font-family:var(--font);
  font-size:var(--fs-small);
  line-height:1.5;
  transition:border-color var(--t), background var(--t);
}
.textarea{min-height:8.5em;resize:vertical;}
.select{
  appearance:none;
  padding-right:38px;
  /* chevron, --paper at 55% — inline SVG can't read a custom property */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23161616' stroke-opacity='.55' stroke-width='1.2'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 14px center;
}
.input::placeholder,.textarea::placeholder{color:var(--dim);opacity:.75;}
.input:hover,.textarea:hover,.select:hover{border-color:var(--paper);}
.input:focus,.textarea:focus,.select:focus{outline:2px solid var(--paper);outline-offset:2px;border-color:var(--paper);}

.input[aria-invalid="true"],.textarea[aria-invalid="true"]{border-color:var(--clip);}
.field-error{font-size:var(--fs-micro);font-weight:500;color:var(--clip);letter-spacing:.03em;}

.check{display:flex;align-items:flex-start;gap:10px;font-size:var(--fs-small);color:var(--dim);}
.check input{
  flex:0 0 auto;
  width:16px;height:16px;
  margin-top:.28em;
  accent-color:var(--paper);
}

/* Form-level feedback. States say what happened, in the site's voice. */
.notice{
  padding:var(--s3) var(--s4);
  margin-bottom:var(--s4);
  background:var(--panel);
  border:1px solid var(--line);
  border-left:2px solid var(--rule);
  border-radius:var(--r);
  font-size:var(--fs-small);
}
.notice-sent{border-left-color:var(--signal);}
.notice-error{border-left-color:var(--clip);}
.notice p:last-child{margin-bottom:0;}

/* ── 11. Masthead ──────────────────────────────────────────────────────── */

/* Zero-height measuring point for the sticky header's shrink state. It has to
   sit OUTSIDE the sticky element — a sentinel inside a `position:sticky` box
   never leaves the viewport, so it would never fire. */
.masthead-sentinel{display:block;height:1px;margin-bottom:-1px;}

/* Sticky, translucent, and shrinks once the page has moved.
 *
 * `position:sticky`, not `fixed`: sticky keeps the header in flow, so nothing
 * needs a compensating top margin and no page can forget to add one. The
 * trade is that the header cannot escape a scrolling ancestor — nothing here
 * has one.
 *
 * The translucency is a legibility device, not decoration: at 78% the ground
 * still separates nav from body copy, and the blur stops a photograph or a
 * spec table reading through it as noise. That is why there is no shadow —
 * §3's restraint holds, the hairline is still the only edge.
 *
 * `.is-stuck` is added by main.js from an IntersectionObserver on a sentinel,
 * so there is no scroll listener. Without JS the header is still sticky and
 * still translucent; it simply never shrinks, which is a smaller state, not a
 * broken one.
 */
.masthead{
  position:sticky;
  top:0;
  z-index:50;
  background:color-mix(in srgb, var(--ink) 84%, transparent);
  border-bottom:1px solid var(--line);
  transition:background var(--t), border-color var(--t);
}
/* No color-mix (older Safari/Firefox): stay fully opaque rather than let body
   copy scroll through an unblurred pane. */
@supports not (background:color-mix(in srgb, red 50%, transparent)){
  .masthead{background:var(--ink);}
}
@supports (backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px)){
  .masthead{
    -webkit-backdrop-filter:saturate(1.4) blur(12px);
    backdrop-filter:saturate(1.4) blur(12px);
  }
}
.masthead.is-stuck{
  background:color-mix(in srgb, var(--ink) 74%, transparent);
  border-bottom-color:var(--rule);
}
@supports not (background:color-mix(in srgb, red 50%, transparent)){
  .masthead.is-stuck{background:var(--ink);}
}

.masthead-inner{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--s3);
  min-height:88px;
  flex-wrap:wrap;
  transition:min-height var(--t);
}
.masthead.is-stuck .masthead-inner{min-height:56px;}

.masthead-mark{
  display:block;
  flex:0 0 auto;
  padding:20px 0;
  transition:padding var(--t);
}
.masthead-mark img{
  display:block;
  width:clamp(146px,19vw,200px);
  transition:width var(--t);
}
/* The wordmark, not `min-height`, is what actually sets the bar's height: at
   200 px wide the logo is 82 px tall, so the expanded bar is ~122 px and the
   88 px minimum never binds. Shrinking the bar therefore means shrinking the
   mark. 108 px keeps "amplifiers" legible on the second line — below about
   96 px it starts to close up. */
.masthead.is-stuck .masthead-mark{padding:9px 0;}
.masthead.is-stuck .masthead-mark img{width:clamp(84px,9vw,108px);}

/* The size change is a state, not an animation — a reduced-motion visitor
   still gets the compact header, just without the tween. */
@media (prefers-reduced-motion:reduce){
  .masthead,
  .masthead-inner,
  .masthead-mark,
  .masthead-mark img{transition:none;}
}

/* Anchors (the applications page's eight case links) must not land under the
   header. Matches the tallest stuck state plus a little air.
   Note `html{scroll-behavior:smooth}` in §2 Base, so these jumps animate.
   That animation makes the anchors look broken when measured too early: a
   reading a few hundred ms after a click catches a 6,000 px scroll mid-flight.
   Wait for scrollY to settle before measuring one.

   Set on the elements, not via `:target` — a `:target` rule only starts
   matching at the moment the browser is already deciding where to scroll, so
   the margin arrives too late and the heading still lands under the header.
   Verified by reading the computed scroll-margin-top, not by looking.

   Why it clears the EXPANDED header (~122 px) and not the stuck one (~62 px):
   a sticky header is in flow, so when it shrinks, everything below it moves up
   by the difference. Click an anchor from the top of the page and the scroll
   lands first, the header shrinks second, and the target rises by ~60 px. A
   margin sized for the stuck bar puts the heading under it; this one clears in
   both states. The cost is some air below the header on anchors clicked while
   already scrolled, which reads as deliberate. */
[id]{scroll-margin-top:132px;}
/* …and once the header is ALREADY compact, no shift is coming, so the big
   margin would just leave a hole. The browser resolves scroll-margin at the
   moment it decides where to scroll, which is exactly when `.is-stuck` is
   already correct — so the two rules pick themselves. `main` is a following
   sibling of `.masthead`, hence `~`. */
.masthead.is-stuck ~ main [id]{scroll-margin-top:76px;}

.sitenav ul{
  display:flex;
  align-items:center;
  gap:clamp(14px,2.3vw,30px);
  list-style:none;
  margin:0;
  padding:0;
}
.sitenav li{margin:0;}
.sitenav a{
  display:block;
  padding:7px 0;
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  /* --paper, not --dim, and it is the transparency above that decided it.
   * A translucent bar means the nav sits on whatever is scrolling under it,
   * and a dark faceplate or a photograph passing beneath drags the effective
   * background down. Measured with notes/masthead-contrast.js: --dim was
   * ALREADY failing at 4.2:1 against dark content at the old 88% opacity,
   * before this got more transparent. --paper clears it with room to spare.
   * The current page is still marked — by the hairline under it, which is the
   * site's own device, rather than by being the only link dark enough to read. */
  color:var(--paper);
  border-bottom:1px solid transparent;
  transition:color var(--t), border-color var(--t);
}
.sitenav a:hover{color:var(--paper);}
.sitenav a[aria-current="page"]{color:var(--paper);border-bottom-color:var(--rule);}

.nav-toggle{
  display:none;
  align-items:center;
  gap:9px;
  padding:9px 16px;
  background:none;
  border:1px solid var(--line);
  border-radius:var(--r);
  /* --paper for the same reason as .sitenav a above: this is the ONLY nav
   * affordance below 820px, it sits on a translucent bar, and --dim measured
   * 3.18:1 against dark content scrolling under it. */
  color:var(--paper);
  font-family:var(--font);
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
  cursor:pointer;
  transition:color var(--t), border-color var(--t);
}
.nav-toggle:hover{color:var(--paper);border-color:var(--rule);}
.nav-toggle-bars{position:relative;display:block;width:16px;height:1px;background:currentColor;}
.nav-toggle-bars::before,
.nav-toggle-bars::after{
  content:'';
  position:absolute;
  left:0;
  width:16px;
  height:1px;
  background:currentColor;
  transition:transform var(--t);
}
.nav-toggle-bars::before{top:-5px;}
.nav-toggle-bars::after{top:5px;}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars{background:transparent;}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before{transform:translateY(5px) rotate(45deg);}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after{transform:translateY(-5px) rotate(-45deg);}

@media (max-width:820px){
  .masthead-inner{min-height:72px;}
  .masthead.is-stuck .masthead-inner{min-height:48px;}
  .nav-toggle{display:inline-flex;}
  .sitenav{flex:1 0 100%;display:none;border-top:1px solid var(--line);}
  .sitenav.is-open{display:block;}
  .sitenav ul{flex-direction:column;align-items:stretch;gap:0;}
  .sitenav li + li{border-top:1px solid var(--line);}
  .sitenav a{padding:15px 0;}

  /* Now that the header is sticky, an open menu is inside a fixed-height
     band rather than at the top of a page that can scroll. Eight items at
     320 × 568 would otherwise run off the bottom with no way to reach the
     last one. */
  .sitenav.is-open{
    max-height:calc(100dvh - 56px);
    overflow-y:auto;
    overscroll-behavior:contain;
  }
  /* An open menu is a solid surface: translucency behind a list of links is
     unreadable in a way it never is behind one row. */
  .masthead:has(.sitenav.is-open){background:var(--ink);}
}

/* ── 12. Page head ─────────────────────────────────────────────────────── */

.page-head{
  padding:var(--head-pad-top) 0 clamp(30px,4.5vw,52px);
  border-bottom:1px solid var(--line);
}
.page-head h1{max-width:24ch;}
.page-head .standfirst{
  margin:var(--s3) 0 0;
  max-width:var(--measure);
  font-size:var(--fs-lede);
  font-weight:300;
  color:var(--dim);
}

/* Home hero. A taller .page-head with the faceplate under it — the panel is
   205.7 × 29.5 mm, so it is a very wide, very short object and wants the full
   measure to itself rather than a column beside the text. The panel is also
   what holds the section open: drop it and the bottom padding has to grow, or
   the photograph crops to a strip.
   Top padding is --head-pad-top, the same token .page-head uses, so the home
   page's first line starts level with every other page's. Do not give this
   its own value. */
.hero{
  padding:var(--head-pad-top) 0 clamp(40px,6vw,72px);
  border-bottom:1px solid var(--line);
}
.hero h1{max-width:18ch;margin-bottom:var(--s4);}
.hero-lede{max-width:56ch;}
.hero-actions{margin-top:var(--s5);}

/* Centred rather than left-aligned: at 1180 px the panel would otherwise sit
   hard left of a lot of empty gutter. z-index because it is a dark object over
   a photograph and must not be the thing that recedes.
   .chassis-caption is listed alongside because the hero has held the chassis
   too — the two are interchangeable here. */
.hero-panel{margin-top:clamp(40px,6vw,76px);position:relative;z-index:1;}
.hero-panel .faceplate-svg{margin:0 auto;}
.hero-panel .faceplate-caption,
.hero-panel .chassis-caption{text-align:center;}

/* "The unit": the object between the section's hairline and the prose. Full
   container width — the chassis sweeps 381 mm through a turn (§18), so it wants
   the whole line rather than a column beside the text.
   No margin-top: .rule-break directly above already contributes --s5, and
   adding to it pushes the object away from the break it belongs under. */
.unit-object{margin-bottom:var(--s6);}

/* Home hero photograph + parallax (2026-07-31, client request — they liked the
 * parallaxing hero on nakymatone.com and asked for the same movement here, but
 * light rather than dark).
 *
 * Three layers: the photograph, a scrim, then the existing content. The scrim
 * is NOT decoration — it is what lets --paper and --dim keep their contrast
 * over a photograph that has black window frames and a dark fireplace in it.
 * It is graded rather than flat so the room still reads on the right-hand side
 * while the text column sits on something close to plain --ink.
 *
 * Measured, not guessed: the darkest pixel under the h1/lede/eyebrow column
 * gives --dim 4.9:1 and --paper 15.3:1 at every viewport (notes/hero-contrast.js).
 * If you weaken the scrim or move the text, re-run that script — it is the
 * whole reason this is allowed to sit under body copy.
 */
/* No `overflow:hidden` here, deliberately — .hero-media clips its own
 * oversized img, and the hero has to let that media box escape upwards (see
 * below). Adding it back silently cancels the photograph behind the header. */
.hero{position:relative;isolation:isolate;}
.hero > .container{position:relative;z-index:1;}

/* The media box runs UP past the top of the hero by exactly the expanded
 * masthead's height (client request, 2026-07-31: "make the header a bit
 * transparent so I can see the room").
 *
 * The masthead was already translucent — 84% --ink with a backdrop blur — but
 * it is the first thing in the document, so at scroll 0 the only thing behind
 * it was the body's own --ink. Translucency over the same colour is opacity.
 * Nothing about the header needed changing; the photograph needed somewhere to
 * be. Overshooting is harmless (the excess sits above the document top and is
 * simply never painted) but undershooting is not — it leaves a hard seam
 * across the bar where the picture starts — so --masthead-h errs upward.
 *
 * The header still paints over all of this: it carries z-index:50 in the root
 * stacking context and .hero's `isolation` keeps the whole hero, media and
 * all, below it. */
.hero-media{
  position:absolute;
  inset:calc(-1 * var(--masthead-h)) 0 0 0;
  z-index:0;
  overflow:hidden;
  pointer-events:none;
}

/* 118% tall, inset -9%: the image is deliberately bigger than its box so the
 * parallax has somewhere to travel. --p is 0 when the hero's top meets the
 * viewport top and 1 when its bottom does; JS writes only that number, so the
 * geometry stays here. Travel is ±6%, comfortably inside the 9% margin. */
.hero-media img{
  position:absolute;
  top:-9%;
  left:0;
  width:100%;
  height:118%;
  max-width:none;                    /* §2's reset caps img at 100% */
  object-fit:cover;
  object-position:58% 46%;
  transform:translate3d(0, calc(var(--p, .5) * 12% - 6%), 0);
}

/* The scrim starts at the HERO's top edge, not the media box's — the band
 * above it is the masthead's, and gets its own much lighter wash below. Two
 * reasons it is inset rather than simply covering everything: the 90% left-hand
 * end of this grade would leave the bar as opaque as it was before, and keeping
 * the scrim's geometry identical to the hero box is what keeps the measured
 * hero readings (notes/hero-contrast.js) meaning what they said. */
.hero-scrim{
  position:absolute;
  inset:var(--masthead-h) 0 0 0;
  background:
    linear-gradient(180deg, rgba(var(--ink-rgb),0) 42%, rgba(var(--ink-rgb),.68) 100%),
    linear-gradient(90deg,
      rgba(var(--ink-rgb),.90) 0%,
      rgba(var(--ink-rgb),.86) 42%,
      rgba(var(--ink-rgb),.58) 74%,
      rgba(var(--ink-rgb),.42) 100%);
}

/* The masthead band's own wash. It follows the same left-to-right direction as
 * the scrim below — dense under the wordmark, open on the right — at roughly
 * half the strength, because the header adds 84% --ink of its own on top: .52
 * here composites to ~81% behind the wordmark and ~72% behind the nav, which
 * is a pane you can see a room through rather than a picture.
 *
 * This is the one place on the site where nav text and the black wordmark
 * sit on a photograph. It was set by measurement, not by eye — the same trap
 * notes/hero-contrast.js exists for. notes/masthead-contrast.js reads the real
 * pixels behind every nav item; run it (with a scrollY=0 sample, which its
 * default STEP loop skips) after touching either alpha here or §11's.
 *
 * A pseudo-element rather than a third div: it is a wash, not content, and it
 * belongs to .hero-media's box, which is what defines the band. */
.hero-media::after{
  content:'';
  position:absolute;
  top:0;
  left:0;
  right:0;
  height:var(--masthead-h);
  background:linear-gradient(90deg,
    rgba(var(--ink-rgb),.52) 0%,
    rgba(var(--ink-rgb),.48) 42%,
    rgba(var(--ink-rgb),.26) 74%,
    rgba(var(--ink-rgb),.16) 100%);
}

/* THE HERO'S SECONDARY TEXT IS --paper HERE, NOT --dim, AND THAT IS NOT A
 * STYLE PREFERENCE — IT IS ARITHMETIC.
 *
 * --dim is #666666. Over the darkest pixel this photograph contains, a scrim
 * has to reach about 92% opacity before #666 clears 4.5:1 — and a 92% scrim
 * is not a photograph any more, it is a pale rectangle. --paper (#161616)
 * clears 4.5:1 at roughly 45%, which leaves room for the picture to be a
 * picture. So the eyebrow, the lede and the visual's caption go dark inside
 * the hero and stay --dim everywhere else on the site.
 *
 * The first version of this kept --dim and graded the scrim instead. It looked
 * completely fine in a screenshot and measured 2.44:1 at 1024px — which is the
 * argument for notes/hero-contrast.js existing at all.
 *
 * Any caption placed in the hero needs a selector here. Both are listed
 * because the hero visual has been each of them in turn. */
.hero .eyebrow,
.hero .hero-lede,
.hero .faceplate-caption,
.hero .chassis-caption{color:var(--paper);}

/* Below the nav breakpoint the text column spans the full width, so a
 * left-to-right grade protects nothing. Go flat and let the room read as
 * texture rather than as a picture. */
@media (max-width:820px){
  .hero-scrim{
    background:
      linear-gradient(180deg, rgba(var(--ink-rgb),.78) 0%, rgba(var(--ink-rgb),.86) 100%);
  }
  /* Flat here for the same reason as the scrim: with the wordmark on the left
     and the Menu button on the right there is no clear side to open up. */
  .hero-media::after{background:rgba(var(--ink-rgb),.44);}
  .hero-media img{object-position:60% 42%;}
}

@media (prefers-reduced-motion:reduce){
  .hero-media img{transform:none;}
}

/* Placeholder marker for an unbuilt page. */
.stub{
  display:inline-flex;
  align-items:center;
  gap:10px;
  padding:8px 18px;
  border:1px dashed var(--rule);
  border-radius:var(--r);
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
}

/* ── 13. 404 ───────────────────────────────────────────────────────────── */

.notfound{padding:clamp(56px,11vw,140px) 0;}
.notfound .code{
  margin:0 0 var(--s4);
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track-wide);
  text-transform:uppercase;
  color:var(--dim);
}
.notfound h1{max-width:20ch;}
.notfound p{max-width:44ch;color:var(--dim);font-weight:300;}
.notfound .btn-row{margin-top:var(--s5);}

/* ── 14. Footer ────────────────────────────────────────────────────────── */

.sitefoot{
  /* No margin-top: sections carry their own bottom padding, and doubling the
     two left a dead band above the footer on short pages. */
  border-top:1px solid var(--line);
  padding:clamp(28px,4vw,44px) 0;
  font-size:var(--fs-small);
  color:var(--dim);
}
.sitefoot-inner{
  display:flex;
  align-items:center;
  justify-content:space-between;
  flex-wrap:wrap;
  gap:var(--s3) var(--s5);
}
.sitefoot p{margin:0;letter-spacing:.03em;}
.sitefoot-brand{color:var(--paper);}
.sitefoot-brand a{border-bottom:1px solid var(--line);transition:border-color var(--t);}
.sitefoot-brand a:hover{border-bottom-color:var(--paper);}
.sitefoot-nav ul{
  display:flex;
  flex-wrap:wrap;
  gap:var(--s2) var(--s4);
  list-style:none;
  margin:0;
  padding:0;
}
.sitefoot-nav li{margin:0;}
.sitefoot-nav a{
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  transition:color var(--t);
}
.sitefoot-nav a:hover{color:var(--paper);}
.sitefoot-legal{flex:1 0 100%;font-size:var(--fs-micro);letter-spacing:.05em;}

@media (max-width:820px){
  .sitefoot-inner{flex-direction:column;align-items:flex-start;}
}

/* ── 15. Faceplate ─────────────────────────────────────────────────────── */
/* The SVG carries its own geometry in millimetres; everything here is colour
   and sizing only. Panel colours are the artwork's: Pantone 100% Black flood,
   Pantone 7541 C ink. Keep them literal — they are product spec, not theme. */

.faceplate{
  margin:0;
  width:100%;
}
.faceplate-svg{
  display:block;
  width:100%;
  height:auto;
  /* The real panel is 205.7 mm. Past about 4.4 px/mm the 1.4 mm legend caps
     stop looking like silkscreen and start looking like body text. */
  max-width:920px;
}

.fp-panel{fill:#161616;}
.fp-ink{fill:#dfe3e3;}
.fp-hair{stroke:#dfe3e3;fill:none;}
.fp-model{
  font-family:var(--font);
  font-weight:500;
  font-variant-numeric:lining-nums;   /* Raleway defaults to oldstyle figures */
  letter-spacing:0;
}
.fp-legend{font-family:var(--font);font-weight:500;letter-spacing:0;}
.fp-wordmark{fill:#dfe3e3;}

/* --- LED states ---------------------------------------------------------
   These are emission colours seen through a lens on a black panel, which is
   why they are brighter than --signal / --clip: those are for text on the
   light page ground and would read as mud here. Everything is CSS on a data
   attribute — no JS, so a state cannot fall out of sync with its markup.

   The slow pulses run at 2.4-2.8 s. That is well past the 400 ms interface
   motion budget on purpose: this is an ambient indicator imitating hardware,
   the same register as the status pill, not a UI transition.               */

.faceplate{
  --led-green:#46c07a;
  --led-amber:#e0a340;
  --led-red:#e0523a;
  --led-off:#2b2e2e;              /* dark lens, still visible on the panel */
  --led-ink:#dfe3e3;              /* the window as printed artwork */
}

/* States below are the manufacturer's documented behaviour — see the table in
   viz/faceplate.php, from §8.1 of the SD125.4 manual. `amber` and `fault` are
   steady because the manual documents them as steady; `signal` and `clip` are
   the animated stylisations, used by the looping states only. */
.fp-led{fill:var(--led-off);}
.fp-led[data-led-state="ink"]{fill:var(--led-ink);}
.fp-led[data-led-state="on"]{fill:var(--led-green);}
.fp-led[data-led-state="amber"]{fill:var(--led-amber);}
.fp-led[data-led-state="fault"]{fill:var(--led-red);}
.fp-led[data-led-state="breathe"]{fill:var(--led-green);animation:fp-breathe 2.8s ease-in-out infinite;}
.fp-led[data-led-state="signal"]{fill:var(--led-green);animation:fp-signal 2.4s ease-in-out infinite;}
.fp-led[data-led-state="clip"]{fill:var(--led-amber);animation:fp-clip .42s steps(1,end) infinite;}

@keyframes fp-breathe{0%,100%{opacity:.22;}50%{opacity:1;}}
@keyframes fp-signal{0%,100%{opacity:.55;}18%{opacity:1;}42%{opacity:.7;}61%{opacity:1;}}
@keyframes fp-clip{0%,49%{opacity:1;}50%,100%{opacity:.2;}}

/* The hero version: the panel wakes, settles into running, catches one clip,
   recovers. One 14 s loop, staged by delay rather than by script. Long and
   slow is the point — it should read as a unit sitting there working, not as
   an animation demanding attention. */
.faceplate[data-state="demo"] .fp-led[data-led="status"]{animation:fp-demo-status 14s ease-in-out infinite;}
.faceplate[data-state="demo"] .fp-led[data-led="network"]{animation:fp-demo-late 14s ease-in-out infinite;animation-delay:-13.2s;}
.faceplate[data-state="demo"] .fp-led[data-led="wifi"]{animation:fp-demo-late 14s ease-in-out infinite;animation-delay:-12.8s;}
.faceplate[data-state="demo"] .fp-led[data-led="output"]{animation:fp-demo-output 14s ease-in-out infinite;}

@keyframes fp-demo-status{
  0%{opacity:.22;}
  10%{opacity:1;}
  100%{opacity:1;}
}
@keyframes fp-demo-late{
  0%,6%{opacity:.15;}
  14%{opacity:1;}
  100%{opacity:1;}
}
@keyframes fp-demo-output{
  0%,8%{opacity:.15;fill:var(--led-green);}
  16%{opacity:1;fill:var(--led-green);}
  62%{opacity:1;fill:var(--led-green);}
  64%,66%{opacity:1;fill:var(--led-amber);}
  65%,67%{opacity:.25;fill:var(--led-amber);}
  70%{opacity:1;fill:var(--led-green);}
  100%{opacity:1;fill:var(--led-green);}
}

/* The product page version: the unit sits in standby, wakes to running, and
   settles back. One 12 s loop — a different length and a different shape from
   the hero's 14 s, so the two lead panels never read as the same clip playing
   twice on the same site.

   Both halves are §8.1 states and nothing else: standby is Status pulsing
   green with Input and Output dark, running is all five green. Network and
   WiFi never move, because the manual has a network command waking the unit —
   networking cannot be down while it sleeps. That is also what makes the loop
   legible: the two rows that hold still are the reference for the three that
   change.

   Green fill throughout with opacity carrying "unlit", the same trick as
   fp-demo-late — interpolating fill from --led-off to --led-green passes
   through a muddy middle that reads as a fault rather than a wake. */
.faceplate[data-state="wake"] .fp-led[data-led="status"]{animation:fp-wake-status 12s ease-in-out infinite;}
.faceplate[data-state="wake"] .fp-led[data-led="input"]{animation:fp-wake-in 12s ease-in-out infinite;}
.faceplate[data-state="wake"] .fp-led[data-led="output"]{animation:fp-wake-out 12s ease-in-out infinite;}

/* Pulses through standby at roughly the 1.8 s of fp-breathe, then holds steady
   the whole time the unit is running. Starts and ends dim so the loop joins. */
@keyframes fp-wake-status{
  0%{opacity:.22;}
  7.5%{opacity:1;}
  15%{opacity:.22;}
  22.5%{opacity:1;}
  30%{opacity:.22;}
  38%{opacity:1;}
  80%{opacity:1;}
  88%{opacity:.22;}
  94%{opacity:1;}
  100%{opacity:.22;}
}
/* Input leads output by ~3% coming up and trails it going down: signal arrives,
   then the output follows it; the output mutes, then the input goes. */
@keyframes fp-wake-in{
  0%,32%{opacity:.15;}
  40%{opacity:1;}
  78%{opacity:1;}
  86%{opacity:.15;}
  100%{opacity:.15;}
}
@keyframes fp-wake-out{
  0%,35%{opacity:.15;}
  43%{opacity:1;}
  75%{opacity:1;}
  82%{opacity:.15;}
  100%{opacity:.15;}
}

.faceplate-caption{
  margin-top:var(--s2);
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
}

/* ── 16. Power Share ───────────────────────────────────────────────────── */
/* One track per channel pair = one 250 W budget. Ch A fills from the left, Ch B
   from the right, and unspent budget is the gap between them. The even split
   therefore meets in the middle and a collapsed pair fills the track from one
   end — the two published endpoints, read off the same graphic.

   No indicator colour here on purpose: allocation is a quantity, not a state,
   and --signal / --clip mean LED state.

   Both channels fill in the SAME ink, and that is load-bearing. The first pass
   gave Ch B a lighter --dim fill so the two ends could be told apart, which put
   a black half and a grey half either side of an even split — the one reading
   the graphic exists to make obvious. Equal power must look equal, so: ink
   length is watts, and the 1 px break between the two fills is the split. That
   break is what powershare.js drags. Which channel is which is carried by the
   wattages printed at each end, not by tone. */

.powershare{margin:0;}

.ps-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s2) var(--s3);
  flex-wrap:wrap;
  margin-bottom:var(--s4);
  padding-bottom:var(--s2);
  border-bottom:1px solid var(--line);
}
.ps-mode{
  font-size:var(--fs-micro);
  font-weight:500;
  letter-spacing:var(--track);
  text-transform:uppercase;
  color:var(--dim);
  font-variant-numeric:tabular-nums lining-nums;
}

.ps-pairs{display:flex;flex-direction:column;gap:var(--s4);}

.ps-pair-head{
  display:flex;
  align-items:flex-end;
  justify-content:space-between;
  gap:var(--s2);
  margin-bottom:10px;
}
.ps-ch{display:flex;flex-direction:column;gap:3px;min-width:0;}
.ps-ch-b{align-items:flex-end;text-align:right;}

.ps-w{
  font-size:clamp(1.2rem,2.4vw,1.7rem);
  font-weight:300;
  line-height:1;
  letter-spacing:-.02em;
  font-variant-numeric:tabular-nums lining-nums;
  white-space:nowrap;
}
.ps-w small{font-size:.5em;font-weight:500;letter-spacing:.04em;margin-left:.32em;color:var(--dim);}
.ps-w-zero{color:var(--dim);}

.ps-budget{
  flex:0 1 auto;
  padding-bottom:2px;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
  text-align:center;
  white-space:nowrap;
  font-variant-numeric:tabular-nums lining-nums;
}

/* The handle can't live inside the track — the track clips its fills — so the
   slider wrapper is the positioning context. It exists in both modes. */
.ps-slider{position:relative;}

.ps-track{
  display:flex;
  height:16px;
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r);
  overflow:hidden;
}
.ps-seg{display:block;height:100%;flex:0 0 auto;}
.ps-seg-a,.ps-seg-b{background:var(--paper);}
/* The split, marked on B's leading edge. Inset rather than a border so a 0 W
   channel takes no width at all, and so it disappears when the two fills are
   not touching — with budget unspent between them there is no split to mark. */
.ps-seg-b{box-shadow:inset 1px 0 0 var(--ink);}
.ps-seg-gap{flex:1 1 auto;background:none;}

/* On a --panel band the unfilled track would be the same colour as the section
   behind it, so unspent budget — the whole point of the gap — would vanish.
   Same correction the design system already makes for .card and .notice. */
.sec-panel .ps-track{background:var(--ink);}

.ps-headroom{
  margin:8px 0 0;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
}

/* --- Interaction --------------------------------------------------------
   Everything below is revealed by `is-live`, which powershare.js adds once it
   has wired a figure up. So a page whose JS is blocked or broken shows the
   static chart rather than controls that do nothing. */

.ps-handle,
.ps-presets{display:none;}

.powershare.is-live .ps-slider{
  /* The padding is the touch target: pointerdown anywhere on the rail grabs the
     split, so the thing a finger aims at is the whole 16 px track, not the 3 px
     handle — 14 px either side takes it to 44. Horizontal padding stays 0
     because the handle's `left` percentage resolves against this box. */
  padding:14px 0;
  cursor:ew-resize;
  /* pan-y, not none: a horizontal drag is ours, a vertical swipe still
     scrolls the page. `none` here makes the component a scroll trap. */
  touch-action:pan-y;
}
/* The padding replaces the static layout's gap under the head. */
.powershare.is-live .ps-pair-head{margin-bottom:0;}

/* A rectangle, because rectangles act (main.css §6). */
.powershare.is-live .ps-handle{
  display:block;
  position:absolute;
  top:50%;
  width:3px;
  height:30px;
  margin-left:-1.5px;               /* no transform: left is animated */
  transform:translateY(-50%);
  background:var(--paper);
  border-radius:0;
}
/* Grab area over the 3 px bar. 28 px, not 44: at the ends of the travel the
   handle sits on the track's edge, and a 44 px box overhung the page gutter far
   enough to add 2 px of horizontal scroll at 320–390 px. The rail's padding
   above is what provides the 44 px target. */
.powershare.is-live .ps-handle::before{
  content:'';
  position:absolute;
  top:50%;
  left:50%;
  width:28px;
  height:44px;
  transform:translate(-50%,-50%);
}
/* Square, because the ring inherits the element's radius and Chrome adds the
   offset to it — 1 px of radius on a 3 px bar came out a stadium, and a pill
   is the one shape reserved for things that report rather than act (§6/§7). */
.powershare.is-live .ps-handle:focus-visible{outline-offset:4px;border-radius:0;}

.powershare.is-live .ps-presets{
  display:flex;
  flex-wrap:wrap;
  gap:var(--s2) var(--s3);
  align-items:center;
  margin-top:var(--s4);
}
.ps-preset{padding:10px 18px;}
.ps-preset[aria-pressed="true"]{
  background:var(--paper);
  color:var(--ink);
  border-color:var(--paper);
}

/* Keyboard and preset jumps glide; a drag must not, or the fill lags the
   finger. The global reduced-motion rule already flattens both. */
.powershare.is-live .ps-seg,
.powershare.is-live .ps-handle{transition:width var(--t), left var(--t);}
.powershare.is-dragging .ps-seg,
.powershare.is-dragging .ps-handle{transition:none;}

.ps-foot{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s2) var(--s3);
  flex-wrap:wrap;
  margin-top:var(--s5);
  padding-top:var(--s3);
  border-top:1px solid var(--line);
}
.ps-total{
  font-size:clamp(1.2rem,2.4vw,1.7rem);
  font-weight:300;
  line-height:1;
  letter-spacing:-.02em;
  font-variant-numeric:tabular-nums lining-nums;
}
.ps-total small{font-size:.5em;font-weight:500;letter-spacing:.04em;margin-left:.32em;color:var(--dim);}

.powershare-caption{
  margin:var(--s3) 0 0;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  line-height:1.6;
  color:var(--dim);
  max-width:var(--measure);
}

/* Three items in a row is one too many on a phone: the shared-budget label is
   the one that can go, because the track's width says the same thing. */
@media (max-width:520px){
  .ps-budget{display:none;}
}

/* ── 17. Zone diagram ──────────────────────────────────────────────────── */
/* Each zone keeps the same shape at every width — channel and wattage on one
   line, name under it, the run, then the load. That is the Power Share block's
   grammar on purpose: the two sit on the same pages and should read as
   siblings. Only the run is drawn; everything else is text, which is why this
   survives 320 px where a fan-out SVG would not.

   Every mode is visible and stacked until zones.js collapses them into tabs,
   so a reader with no JavaScript gets both diagrams rather than a dead switch. */

.zones{margin:0;}

.zn-switch{display:none;}
.zones.is-live .zn-switch{
  display:flex;
  flex-wrap:wrap;
  gap:var(--s2) var(--s3);
  margin-bottom:var(--s4);
}
.zn-tab{padding:10px 18px;}
.zn-tab[aria-selected="true"],
.zn-tab[aria-pressed="true"]{
  background:var(--paper);
  color:var(--ink);
  border-color:var(--paper);
}
.zones.is-live .zn-mode:focus-visible{outline-offset:6px;}

/* Stacked mode: each panel needs its own separation. Once tabbed, only one is
   ever on screen, so the rule between them would be a rule to nowhere. */
.zn-mode + .zn-mode{
  margin-top:var(--s5);
  padding-top:var(--s5);
  border-top:1px solid var(--line);
}
.zones.is-live .zn-mode + .zn-mode{margin-top:0;padding-top:0;border-top:0;}

.zn-mode-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s2) var(--s3);
  flex-wrap:wrap;
  margin-bottom:var(--s4);
  padding-bottom:var(--s2);
  border-bottom:1px solid var(--line);
}
.zn-mode-title{
  margin:0;
  font-size:var(--fs-micro);
  font-weight:600;
  letter-spacing:var(--track);
  text-transform:uppercase;
}
.zn-mode-note{
  margin:0;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
  font-variant-numeric:tabular-nums lining-nums;
}

.zn-zones{display:flex;flex-direction:column;gap:var(--s4);}

.zn-zone-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s2);
}
.zn-w{
  font-size:1.05rem;
  font-weight:400;
  line-height:1;
  letter-spacing:-.01em;
  font-variant-numeric:tabular-nums lining-nums;
  white-space:nowrap;
}
.zn-w small{font-size:.6em;font-weight:500;letter-spacing:.04em;margin-left:.3em;color:var(--dim);}

.zn-name{
  margin:6px 0 10px;
  font-size:var(--fs-small);
  line-height:1.4;
}
/* The brand is an optional field — a zone without one must
   look deliberate, not broken, so it is a suffix rather than a slot.

   It was --dim, which read as a footnote on the room name. The client's first
   review: "enlarge or BOLD NAKYMATONE, so it is defined — think like a virgin
   viewer." A reader who lands on this page first has no idea these are
   loudspeaker brands, and a greyed-out proper noun is the one weight that
   guarantees they never find out. Full ink at 500 now, still the same size, so
   it reads as a NAME beside the room without outranking it. The separator
   stays quiet — it is punctuation, not content. */
.zn-brand{color:var(--paper);font-weight:500;}
.zn-brand::before{content:'·';margin:0 .5em;color:var(--rule);font-weight:400;}

/* The run: a channel output, then a hairline carrying one marker per speaker.
   Run LENGTH is speaker count — `--n` comes from the markup and the pitch is
   fluid — so a domestic pair is a short stub and a 100 V chain reaches most of
   the way across. Spreading every run evenly across the full width instead
   produced four identical hairlines and threw the comparison away. */
.zones{--zn-pitch:clamp(24px, 3.6vw, 56px);}

.zn-run{
  display:flex;
  align-items:center;
  gap:12px;
  height:14px;
}
/* The channel output the run leaves from. Without it the hairline reads as an
   axis rather than a cable. */
.zn-out{
  flex:0 0 auto;
  width:2px;
  height:12px;
  background:var(--paper);
}
.zn-line{
  position:relative;
  display:flex;
  justify-content:space-between;
  align-items:center;
  /* flex-shrink does the capping: the natural width is count × pitch, and a
     16-speaker chain simply compresses instead of overflowing at 320 px. */
  flex:0 1 auto;
  width:calc(var(--n, 0) * var(--zn-pitch));
  max-width:100%;
  height:100%;
}
.zn-line::before{
  content:'';
  position:absolute;
  left:-12px;                 /* back under .zn-out, across the flex gap */
  right:0;
  top:50%;
  height:1px;
  background:var(--rule);
}
.zn-spk{
  position:relative;
  width:7px;
  height:7px;
  flex:0 0 auto;
  background:var(--paper);
  /* Square: the circles in this system are LED/state readouts (§7), and a
     speaker on a run is neither a state nor something you can click. */
  border-radius:0;
  box-shadow:0 0 0 3px var(--ink);   /* punches the hairline out behind it */
}

.zn-detail{
  margin:8px 0 0;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  color:var(--dim);
  font-variant-numeric:tabular-nums lining-nums;
}

.zn-foot{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--s2) var(--s3);
  flex-wrap:wrap;
  margin-top:var(--s4);
  padding-top:var(--s3);
  border-top:1px solid var(--line);
}
.zn-total{
  font-size:1.05rem;
  font-weight:400;
  line-height:1;
  font-variant-numeric:tabular-nums lining-nums;
}
.zn-total small{font-size:.6em;font-weight:500;letter-spacing:.04em;margin-left:.3em;color:var(--dim);}

.zones-caption{
  margin:var(--s3) 0 0;
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  line-height:1.6;
  color:var(--dim);
  max-width:var(--measure);
}

/* A panel band paints --panel behind the figure, so the marker's knockout has
   to match its ground or every speaker gets a halo. */
.sec-panel .zn-spk{box-shadow:0 0 0 3px var(--panel);}

/* ── 18. Chassis (3D) ──────────────────────────────────────────────────── */
/* The SD125.4 as a rotatable object. See viz/chassis3d.php for what is real
   here (proportions, front panel, rear panel) and what is invented (the sides).

   The box is modelled at the chassis's true proportions — 44 x 220 x 319 mm —
   expressed as CSS variables so one number changes the model. Faces are
   positioned by translating each one out to half of the relevant dimension,
   which is the whole trick: no face needs to know about any other. */

.chassis{
  --w:220;                       /* chassis, in millimetres */
  --h:44;
  --d:319;
  /* One millimetre on screen. Kept modest on purpose: at 2.6px the box was
     570 x 830 px and the perspective turned it into a slab — a half-rack 1U
     really is deeper than it is wide (220 vs 319 mm), so the projection has
     to be gentle or the true proportion reads as a distortion. */
  /* The projected width of a rotating box is w·|cos| + d·|sin|, which peaks
     near 45° at about (220+319)/√2 = 381 mm — NOT at 319 mm. Size for that
     peak or the object clips its container mid-turn, which is exactly what
     happened at 390 px on the first pass. `body{overflow-x:hidden}` hid the
     symptom, so this was found by measuring the box through a full rotation
     rather than by looking at a screenshot. */
  --mm:clamp(0.68px, .22vw, 1.85px);
  --bw:calc(var(--w) * var(--mm));
  --bh:calc(var(--h) * var(--mm));
  --bd:calc(var(--d) * var(--mm));
  margin:0;
}

/* Perspective lives on the stage so the drag handler only ever writes --ry. */
.chassis-stage{
  /* 2600px against a ~590px-deep object: enough recession to read as three
     dimensional, not so much that the far end vanishes. */
  perspective:2600px;
  perspective-origin:50% 46%;
  display:flex;
  justify-content:center;
  align-items:center;
  /* The stage HAS to reserve this height: `.chassis-box` is only --bh tall
     in layout terms (44 mm), and its transformed faces contribute nothing to
     flow, so without a floor here the section would collapse to a sliver and
     the box would overlap its neighbours.
     The projected height of the object is almost exactly 0.38 x --bd through a
     full turn (the depth, foreshortened by the 11 deg tilt, dominates the
     44 mm of chassis) — measured, not derived. So this is that plus one step
     of air, and no more: the first version reserved .46 x --bd PLUS 2 x --bh
     PLUS 24 px of padding, which came to 434 px of stage around a 225 px
     object. */
  min-height:calc(var(--bd) * .40 + var(--s4));
}

.chassis-box{
  position:relative;
  width:var(--bw);
  height:var(--bh);
  transform-style:preserve-3d;
  /* -14deg of X gives the slight top-down look of something on a shelf just
     below eye level; --ry is the only value the script touches. */
  transform:rotateX(-11deg) rotateY(var(--ry, -32deg));
  cursor:grab;
  touch-action:none;             /* the drag is ours, not the browser's */
  /* The -11 deg tilt shows the top face, so the object's visual mass hangs
     BELOW the box's own centre — 120 px of air above against 90 px below when
     it was simply centred. This lifts it by the measured difference, which
     scales with the model, so the air is even at every width. */
  /* .023 is a compromise, not a derivation: the ideal lift is ~.024 x --bd at
     1440 and ~.011 at 390, because the perspective magnification depends on
     the object's absolute size against a fixed 2600 px camera. This lands both
     within ~3 px of balanced, which is imperceptible. */
  top:calc(var(--bd) * -.023);
}
.chassis-box:active{cursor:grabbing;}
.chassis-box:focus-visible{outline:2px solid var(--paper);outline-offset:calc(var(--mm) * 14);}

.chassis-face{
  position:absolute;
  background:#161616;            /* the panel flood, deliberately literal */
  backface-visibility:hidden;
}

/* Front and rear: the full face of the chassis, with the artwork centred on
   it the way the Lexan sits on the real panel. */
.chassis-front,
.chassis-rear{
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  overflow:hidden;
}
.chassis-front{transform:translateZ(calc(var(--bd) / 2));}
.chassis-rear{transform:rotateY(180deg) translateZ(calc(var(--bd) / 2));}

/* 205.7 mm of faceplate on a 220 mm face — the real inset, not a guess. */
.chassis-front .chassis-plate{width:calc(205.7 * var(--mm));}
.chassis-front .chassis-plate svg{display:block;width:100%;height:auto;}
/* The rear Lexan is 216.75 mm and nearly fills its face. */
.chassis-rear img{width:calc(216.75 * var(--mm));height:auto;display:block;}

/* Sides. Depth x height, hinged at the box's left and right edges. */
.chassis-left,
.chassis-right{
  top:0;
  left:calc(var(--bw) / 2 - var(--bd) / 2);
  width:var(--bd);
  height:var(--bh);
}
.chassis-left{transform:rotateY(-90deg) translateZ(calc(var(--bw) / 2));}
.chassis-right{transform:rotateY(90deg) translateZ(calc(var(--bw) / 2));}

/* Top and bottom. Width x depth. */
.chassis-top,
.chassis-bottom{
  left:0;
  top:calc(var(--bh) / 2 - var(--bd) / 2);
  width:var(--bw);
  height:var(--bd);
}
.chassis-top{transform:rotateX(90deg) translateZ(calc(var(--bh) / 2));}
.chassis-bottom{transform:rotateX(-90deg) translateZ(calc(var(--bh) / 2));}

/* The one piece of faked lighting: a flat box with six identical faces reads
   as a silhouette, not an object. Two steps of brightness are enough to give
   it edges — no gradients, which §3 rules out and which would look like
   rendering rather than drawing. */
.chassis-top{background:#222222;}
.chassis-left,
.chassis-right{background:#0e0e0e;}
.chassis-bottom{background:#0a0a0a;}

.chassis-caption{
  margin:var(--s3) auto 0;
  max-width:var(--measure);
  font-size:var(--fs-micro);
  letter-spacing:.04em;
  line-height:1.6;
  color:var(--dim);
  text-align:center;
}

/* Idle rotation. Added by chassis3d.js and removed the moment a drag starts,
   so the two never fight over the same property. */
.chassis-box.is-spinning{animation:chassis-spin 44s linear infinite;}
@keyframes chassis-spin{
  from{transform:rotateX(-11deg) rotateY(0deg);}
  to{transform:rotateX(-11deg) rotateY(360deg);}
}

@media (prefers-reduced-motion:reduce){
  /* No idle spin — but dragging still works, because that is the visitor
     asking for motion rather than being handed it. */
  .chassis-box.is-spinning{animation:none;}
}

/* ── 19. Motion ────────────────────────────────────────────────────────── */
/* Scroll reveal. The hidden state is gated on .has-js (set by an inline
   script in the head) so content is never invisible without JS. */

.has-js .reveal{
  opacity:0;
  transform:translateY(14px);
  transition:opacity var(--t-reveal), transform var(--t-reveal);
  transition-delay:var(--d,0ms);
}
.has-js .reveal.is-in{opacity:1;transform:none;}

/* Stagger for a row of siblings, without needing per-element markup. */
.reveal-stagger > *:nth-child(2){--d:70ms;}
.reveal-stagger > *:nth-child(3){--d:140ms;}
.reveal-stagger > *:nth-child(4){--d:210ms;}
.reveal-stagger > *:nth-child(5){--d:280ms;}
.reveal-stagger > *:nth-child(6){--d:350ms;}
.reveal-stagger > *:nth-child(n+7){--d:420ms;}

@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto;}
  .has-js .reveal{opacity:1;transform:none;transition:none;}
  /* Stop pulses cleanly at full brightness rather than letting the global
     duration clamp freeze them mid-fade. The faceplate falls back to its
     Static state: every LED steady at its own colour. */
  .pill-live::before{animation:none;opacity:1;}
  .fp-led{animation:none !important;opacity:1;}
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
}

/* ── 20. Page-head treatments ──────────────────────────────────────────── */
/* Three independent treatments that give `.page-head` (§12) more presence,
 * because it is the one part of the site that carries none of the signature:
 * six pages open with the same three lines of text on flat ground.
 *
 * They are here rather than in §12 so each can be lifted out whole. Each has
 * its own sub-block and its own removal note, and NONE of them touches the
 * home `.hero` — that page leads with a photograph and needs nothing.
 *
 * Nothing below alters an existing declaration. `.page-head`'s own
 * `border-bottom` in §12 stays exactly as it was, so deleting all of §20
 * returns the heads to their previous state with no other edit.
 */

/* Shared by 20.2 and 20.3, and repeated in both so either can be deleted
   without breaking the other. Harmless if it ends up declared twice. */
.page-head{position:relative;}

/* ── 20.1 Datum strip ──────────────────────────────────────────────────── */
/* A row of label/value pairs in the page head's bottom hairline, in the
 * silkscreen register. Each one is capped by a short hairline — `.sec-head`'s
 * opening segment (clamp(18px,3vw,34px), §5) reused at small scale, so the
 * strip reads as the same device rather than a new one.
 *
 * Not `.datum` (§8): that is an instrument reading at clamp(1.7rem,3vw,2.5rem)
 * and four of them below a headline would outweigh it. This register is closer
 * to a spec-table row.
 *
 * Markup and data: head-datums.php, fed by `head_datums` in routes.php.
 * TO REMOVE: delete this block and follow the note in head-datums.php.
 */
.head-datums{
  display:flex;
  flex-wrap:wrap;
  gap:var(--s4) clamp(var(--s5), 5vw, var(--s7));
  /* Matches .page-head's own bottom padding, so the strip sits centred
     between the standfirst and the hairline rather than crowding either. */
  margin:clamp(30px,4.5vw,52px) 0 0;
  padding:0;
}
.head-datum{display:flex;flex-direction:column;gap:5px;}
.head-datum::before{
  content:'';
  display:block;
  width:clamp(18px,3vw,34px);
  height:1px;
  margin-bottom:var(--s2);
  background:var(--rule);
}
/* <dt>/<dd> inside a <dl>: the pair is the point, and the browser's default
   <dd> indent is not. */
.head-datum dt,
.head-datum dd{margin:0;}
.head-datum-value{
  font-size:var(--fs-h3);
  font-weight:300;
  line-height:1.25;
  letter-spacing:-.01em;
  font-variant-numeric:tabular-nums lining-nums;
}

/* ── 20.2 Drawing grid ─────────────────────────────────────────────────── */
/* A hairline grid behind the head, masked so it fades out before the copy
 * ends. Spec-sheet paper, not decoration — it is the only surface texture on
 * the site and it never reaches full strength anywhere.
 *
 * The two repeating-linear-gradients are hairline GENERATORS, not colour
 * fills; this is the one place the file's "no gradients" rule is set aside,
 * and only because CSS has no other way to repeat a 1px rule.
 *
 * Ink is 4.5% and lines cover ~3% of the area, so the darkest a background
 * pixel gets is ≈#f2f1ef — `--dim` still clears AA on it. Re-run
 * notes/contrast-audit.js if that figure changes.
 *
 * Origin is the viewport, not the container: a grid that reflowed its phase
 * at every breakpoint would read as misalignment rather than paper.
 *
 * TO REMOVE: delete this block.
 */
.page-head{position:relative;}
.page-head > .container{position:relative;z-index:1;}
.page-head::before{
  --grid-pitch:clamp(28px,4vw,44px);
  --grid-ink:rgba(20,20,20,.045);
  content:'';
  position:absolute;
  inset:0;
  z-index:0;
  pointer-events:none;
  background-image:
    repeating-linear-gradient(to right,  var(--grid-ink) 0 1px, transparent 1px var(--grid-pitch)),
    repeating-linear-gradient(to bottom, var(--grid-ink) 0 1px, transparent 1px var(--grid-pitch));
  -webkit-mask-image:linear-gradient(to bottom, #000 0%, rgba(0,0,0,.35) 55%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 0%, rgba(0,0,0,.35) 55%, transparent 100%);
}

/* ── 20.3 Arrival ──────────────────────────────────────────────────────── */
/* The head's hairline draws in from the left and its lines rise behind it.
 *
 * CSS animation rather than the §19 `.reveal` observer, deliberately: the page
 * head is always above the fold, so an intersection observer would fire
 * immediately anyway, and this way the treatment needs no JS, no `.has-js`
 * gate and no markup at all.
 *
 * The drawn rule is a SECOND hairline in `--rule` over the quiet `--line`
 * border §12 already draws — which is why removing this block leaves a correct
 * page head rather than one with no bottom edge. Two hairlines, one quiet and
 * one deliberate, is also what the faceplate does.
 *
 * The stagger mirrors `.reveal-stagger` (§19) at the same 70ms step, so the
 * head and the content below it move to one rhythm.
 *
 * TO REMOVE: delete this block.
 */
@keyframes head-rule-draw{from{transform:scaleX(0);} to{transform:scaleX(1);}}
@keyframes head-rise{from{opacity:0;transform:translateY(14px);} to{opacity:1;transform:none;}}

.page-head{position:relative;}
.page-head::after{
  content:'';
  position:absolute;
  left:0;
  right:0;
  bottom:-1px;              /* exactly over §12's border, not beside it */
  height:1px;
  background:var(--rule);
  transform-origin:left center;
  animation:head-rule-draw .9s cubic-bezier(.22,.7,.3,1) 120ms both;
}
/* --t-reveal carries both the duration and the easing. */
.page-head > .container > *{animation:head-rise var(--t-reveal) both;}
.page-head > .container > *:nth-child(2){animation-delay:70ms;}
.page-head > .container > *:nth-child(3){animation-delay:140ms;}
.page-head > .container > *:nth-child(4){animation-delay:210ms;}
.page-head > .container > *:nth-child(n+5){animation-delay:280ms;}

/* §19's global clamp shortens durations but not delays, which would leave the
   last line invisible for 280ms. Cancel the animations outright instead. */
@media (prefers-reduced-motion: reduce){
  .page-head::after,
  .page-head > .container > *{animation:none;}
  .page-head::after{transform:none;}
}

/* Print. A fill-mode of `both` can leave an unrun animation at opacity 0 on
   paper, and the grid is toner spent on nothing. */
@media print{
  .page-head::before{display:none;}
  .page-head::after,
  .page-head > .container > *{animation:none;}
  .page-head::after{transform:none;}
  .head-datums{gap:var(--s3) var(--s5);}
}

/* ── 21. Reference marks & the Terms footnote ──────────────────────────── */
/* Added at the client's first review: UMAC™ and UREC™ are manufacturer
   trademarks a reader cannot look up, so each carries a numbered mark to a
   footnote. Markup and copy are in glossary.php.

   The mark is a LINK, not a <sup>: superscript at this type size lands under
   the 24 px touch minimum and, worse, at about 8 px it is below the size the
   contrast audit treats as body text. So it is a normal-flow number, raised
   optically by a fraction of an em, sized at --fs-micro, and padded out to a
   real target. Removing all of §21 leaves the marks as plain inline numbers
   and the footnote as a plain ordered list — degraded, not broken. */

.term-ref{
  display:inline-block;
  margin-left:2px;
  padding:0 3px;
  font-size:var(--fs-micro);
  font-weight:500;
  line-height:1;
  /* Optical, not vertical-align:super — super also shrinks line-height in a
     table cell and made the spec rows 2 px taller than their neighbours. */
  transform:translateY(-.42em);
  color:var(--dim);
  border-bottom:1px solid var(--line);
  transition:color var(--t),border-color var(--t);
}
/* --paper is the near-black INK in this token set and --ink the off-white
   ground (§1) — the names read backwards, which is exactly how a hover ends up
   invisible. Matches .link-arrow:hover and .sitenav a:hover. */
.term-ref:hover{color:var(--paper);border-bottom-color:var(--rule);}
/* A mark inside a dark band inherits the band's own re-mapped tokens (§4), so
   nothing else is needed there. */

/* The footnote list. Numbers sit in their own column so the definitions align
   with each other rather than with the digits — the same hanging arrangement
   the datum strip uses. */
.glossary{
  list-style:none;
  margin:var(--s4) 0 0;
  padding:0;
  max-width:var(--measure);
}
.glossary > li{
  display:grid;
  grid-template-columns:1.6rem minmax(0,1fr);
  gap:var(--s2);
  padding-top:var(--s3);
}
.glossary > li + li{
  margin-top:var(--s3);
  border-top:1px solid var(--line);
}
.glossary-num{
  font-size:var(--fs-micro);
  font-weight:500;
  line-height:1.7;
  color:var(--dim);
}
.glossary p{margin:0;font-size:var(--fs-small);line-height:1.65;}
.glossary p + p{margin-top:var(--s2);}
/* :target so following a mark visibly lands somewhere. A left rule rather than
   a background wash: the design has no fills, and a wash on the warm ground
   reads as a highlighter. */
.glossary > li:target{
  border-left:1px solid var(--paper);
  padding-left:var(--s3);
  margin-left:calc(var(--s3) * -1);
}

@media print{
  /* A printed spec sheet is exactly where an unexplained trademark is worst,
     so the footnotes print — but the marks lose their link styling. */
  .term-ref{border-bottom:0;color:#000;}
}

/* ── 22. Print ─────────────────────────────────────────────────────────── */
/* Integrators print spec pages. Make that not embarrassing. */

@media print{
  .masthead,.sitefoot-nav,.nav-toggle,.skip-link,.rack-screw{display:none;}
  body{background:#fff;color:#000;font-size:11pt;}
  .sec{padding:12pt 0;}
  .specs th,.specs td{border-color:#ccc;}
  a{text-decoration:none;color:#000;}
  /* The Power Share bars ARE the information — a printed spec page with the
     backgrounds dropped shows two empty rectangles. */
  .ps-track{border-color:#999;}
  .ps-seg-a,.ps-seg-b{-webkit-print-color-adjust:exact;print-color-adjust:exact;}
  /* An integrator printing the applications page wants both wiring modes, not
     whichever tab happened to be open. */
  .zn-switch{display:none !important;}
  .zn-mode[hidden]{display:block !important;}
  .zn-mode + .zn-mode{margin-top:18pt;padding-top:18pt;border-top:1px solid #ccc;}
  .zn-spk{-webkit-print-color-adjust:exact;print-color-adjust:exact;box-shadow:none;}
}
