/* ---------------------------------------------------------------------------
   nothingbutdatacenters.com

   Two rules hold this together.

   1. TYPE ENCODES THE BOUNDARY. Anything generated from the project record is
      set in mono. Anything a person wrote is set in sans. The README draws
      that line; the typography keeps it.

   2. NOTHING IS BRIGHTER THAN THE RENDER. The pass log reports a maximum
      measured luminance of 179.6 of 255. --lum-max is that value. No text,
      rule or fill on this site exceeds it, so the page cannot be brighter
      than the brightest frame in the work.

   One hue, three fills. Solid amber means a verified manifest. Hollow means
   observed in the log but not verified. Dashed means no record at all. That
   is the whole legend, and it is the record's own epistemology.
--------------------------------------------------------------------------- */

:root {
  /* Greys sampled up the luminance ramp the render actually occupied. */
  --ground:     #0a0a0b;
  --ground-2:   #111112;
  --ground-3:   #171718;
  --rule:       #242425;
  --rule-soft:  #1b1b1c;
  --dim:        #807e79;   /* labels, secondary        4.9:1 */
  --text:       #95938e;   /* body                     6.4:1 */
  --lum-max:    #b4b2ac;   /* headings. luminance 179.6 of 255. the ceiling. */

  /*
     The second hue, and the ONLY one. Reserved for leaving: the close control
     and the scrim that dismisses a panel. Nothing else may use it -- not
     errors, not crashes, not the failed runs, which stay uncoloured on
     purpose. The moment red means two things it means nothing, and the value
     of an exit being the one red thing on the site is that it is unmistakable.

     Held under the luminance ceiling like everything else.
  */
  --exit:       #d15a4e;
  /*
     Lifted from #8f3a33, which measured 2.54:1 against the panel it sits on.
     This is the visible boundary of the close control, and WCAG 1.4.11 asks
     3:1 of anything that identifies a component. #a8463d is the smallest step
     that clears it on both grounds the button appears over -- 3.29:1 on the
     scrim-tinted desktop panel, 3.24:1 on the mobile one -- and it is still
     well under the luminance ceiling. The same arithmetic that lifted --dim
     from #6a6965; contrast here is measured, never eyeballed.
  */
  --exit-dim:   #a8463d;

  --amber:      #e8a33d;
  --amber-mid:  #a8752b;
  --amber-low:  #6d4c1c;
  --amber-wash: rgba(232, 163, 61, 0.09);

  /*
     ONE FACE. Monospace, everywhere, including running prose.

     This went through a text serif and then a neutral grotesque, and both were
     wrong for the same reason: they made the page read as a document ABOUT the
     work instead of an instrument attached to it. The brief is a ship's
     terminal from a 1979 film, rebuilt in the 2030s for datacenter telemetry
     -- so the body text is not a companion to the mono, it IS the mono.

     Everything that makes it feel like a terminal rather than a code listing
     is treatment, not typeface: even colour, open leading, uppercase micro
     labels on wide tracking, amber on near-black, and a block cursor.

     Cascadia Mono leads. It ships with Windows Terminal, which is the closest
     thing to a modern version of the reference, and its zero and one are
     unambiguous on a page that is mostly frame numbers and hashes.
  */
  --mono: "Cascadia Mono", ui-monospace, "SF Mono", "JetBrains Mono",
          "IBM Plex Mono", "Roboto Mono", Consolas, "Lucida Console", monospace;

  /* Phosphor, at the threshold of visible. No scanlines, no curvature, no
     flicker -- those are the costume. This is just the sense that the bright
     text is emitting rather than printed. */
  --glow-warm: 0 0 24px rgba(232, 163, 61, 0.14);
  --glow-cool: 0 0 20px rgba(180, 178, 172, 0.10);

  /* One scale, used everywhere. */
  --t-micro: 0.66rem;   /* eyebrows, legends, tile captions */
  --t-data:  0.76rem;   /* tables, fields, nav */
  --t-body:  0.92rem;   /* mono runs large; this is optically ~1rem of sans */
  --t-lede:  clamp(0.95rem, 0.9rem + 0.35vw, 1.1rem);
  --t-h3:    0.95rem;
  --t-h2:    clamp(1.1rem, 2vw, 1.4rem);
  --t-h1:    clamp(1.6rem, 4.2vw, 2.7rem);

  /* Tracking tightens as size grows, which is what keeps mono from looking
     like code when it is being used as display type. */
  --track-display: -0.045em;
  --track-h1: -0.035em;
  --track-h2: -0.025em;
  --track-label: 0.18em;

  /*
     CURSORS. Inline SVG, so there are no files to ship and nothing to 404.

     The reticle is not decoration borrowed from somewhere: the machine-vision
     overlay in the treatments draws corner brackets on every detection it
     makes, at bracket_len 56. Pointing at a frame puts the same brackets
     around it. The cursor is the site detecting what you are looking at.

     Every one ends in a real keyword, so a browser that refuses the image
     still gets the right affordance.
  */
  /*
     The pointer itself. Bone at rest, amber when something is interactive --
     the same two colours the type uses for the same distinction, so the cursor
     is reading from the palette rather than announcing itself.

     Overriding the default cursor also removes the automatic I-beam over text,
     so the text cursor has to be put back explicitly further down. That is the
     cost of taking the default, and it is worth paying once rather than
     leaving the one generic thing on an otherwise specific page.
  */
  --cur-default: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='26'%3E%3Cpath d='M4 2v18l4.3-4.2 2.8 5.9 3-1.4-2.8-5.8H17z' fill='%23000' fill-opacity='.5' stroke='%23000' stroke-opacity='.5' stroke-width='3' stroke-linejoin='round'/%3E%3Cpath d='M4 2v18l4.3-4.2 2.8 5.9 3-1.4-2.8-5.8H17z' fill='%23b4b2ac'/%3E%3C/svg%3E") 4 2, default;
  /*
     The select cursor is a HAND, not a recoloured arrow. Interactivity should
     be a different silhouette, not a different colour -- a colour-only tell is
     no tell at all to a viewer who cannot separate the two hues, and the hand
     is the one cursor convention worth keeping. Drawn in straight segments so
     it is faceted rather than cartoon, and faceted is the house style.
  */
  --cur-link: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='28'%3E%3Cpath d='M8 2h3v11h3v1h3v1h3v8l-3 3h-7l-4-5-3-5 1-1 4 2z' fill='%23000' fill-opacity='.5' stroke='%23000' stroke-opacity='.5' stroke-width='3' stroke-linejoin='round'/%3E%3Cpath d='M8 2h3v11h3v1h3v1h3v8l-3 3h-7l-4-5-3-5 1-1 4 2z' fill='%23e8a33d' stroke='%230a0a0b' stroke-width='1'/%3E%3C/svg%3E") 9 2, pointer;
  --cur-text: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='26'%3E%3Cg stroke='%23000' stroke-opacity='.5' stroke-width='4'%3E%3Cpath d='M9 4v18M5 4h8M5 22h8'/%3E%3C/g%3E%3Cg stroke='%23b4b2ac' stroke-width='1.6'%3E%3Cpath d='M9 4v18M5 4h8M5 22h8'/%3E%3C/g%3E%3C/svg%3E") 9 13, text;
  /* Measurement, for the frame axis: it is a ruler, so it gets a ruler's cursor. */
  --cur-cross: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Cg stroke='%23000' stroke-opacity='.5' stroke-width='4'%3E%3Cpath d='M13 1v9M13 16v9M1 13h9M16 13h9'/%3E%3C/g%3E%3Cg stroke='%23e8a33d' stroke-width='1.6'%3E%3Cpath d='M13 1v9M13 16v9M1 13h9M16 13h9'/%3E%3C/g%3E%3C/svg%3E") 13 13, crosshair;

  --cur-inspect: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.55' stroke-width='4'%3E%3Cpath d='M4 10V4h6M26 10V4h-6M4 20v6h6M26 20v6h-6'/%3E%3C/g%3E%3Cg fill='none' stroke='%23e8a33d' stroke-width='2'%3E%3Cpath d='M4 10V4h6M26 10V4h-6M4 20v6h6M26 20v6h-6'/%3E%3C/g%3E%3Ccircle cx='15' cy='15' r='1.6' fill='%23e8a33d'/%3E%3C/svg%3E") 15 15, zoom-in;
  --cur-prev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.55' stroke-width='5'%3E%3Cpath d='M18 7l-8 8 8 8'/%3E%3C/g%3E%3Cg fill='none' stroke='%23e8a33d' stroke-width='2.4'%3E%3Cpath d='M18 7l-8 8 8 8'/%3E%3C/g%3E%3C/svg%3E") 15 15, w-resize;
  --cur-next: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.55' stroke-width='5'%3E%3Cpath d='M12 7l8 8-8 8'/%3E%3C/g%3E%3Cg fill='none' stroke='%23e8a33d' stroke-width='2.4'%3E%3Cpath d='M12 7l8 8-8 8'/%3E%3C/g%3E%3C/svg%3E") 15 15, e-resize;
  --cur-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.55' stroke-width='5'%3E%3Cpath d='M9 9l12 12M21 9L9 21'/%3E%3C/g%3E%3Cg fill='none' stroke='%23d15a4e' stroke-width='2.4'%3E%3Cpath d='M9 9l12 12M21 9L9 21'/%3E%3C/g%3E%3C/svg%3E") 15 15, zoom-out;

  --gut: clamp(1.25rem, 4vw, 3.25rem);
  --measure: 36rem;
  --max: 74rem;
  /* The sticky masthead's height. Anything else that sticks hangs off this. */
  --masthead-h: 3.4rem;

  /*
     MOTION.

     Every animation on this site composes from what follows. There are no
     bespoke timing values anywhere else in this file -- that is the rule, and
     it is checkable rather than claimed: grep this file for a duration outside
     the token block and the only hits are the four lines of the
     reduced-motion backstop at the bottom.

     The audit that produced these is MOTION_AUDIT.md. The short version: there
     were thirteen transition durations, six easings and six travel distances,
     each one chosen locally and defensibly, and no two components agreeing. A
     press took 70ms in one place, 90ms in another, 100ms in a third and 120ms
     in a fourth. Nothing was wrong on its own. The whole read as uncalibrated,
     which on something pretending to be an instrument is the thing that costs
     it credibility.
  */

  /*
     THREE DURATIONS, as raw values. These are the only motion numbers in the
     file; everything below is arithmetic on them.

     Feedback -- a press, a colour moving under the pointer. Has to resolve
     before the eye gets around to asking whether it happened.
  */
  --m-t-fast: 90ms;
  /* State change. A hover lift, the reticle closing, a panel opening. Long
     enough to read as a movement, short enough never to gate the next action. */
  --m-t-base: 180ms;
  /* Arrival. Something entering that was not there before. The only tier
     allowed to be noticed as a duration. */
  --m-t-slow: 420ms;

  /*
     AMBIENT PERIODS -- a different axis.

     These describe how often something RECURS, not how long a response takes.
     Folding them into the three-tier scale would be a category error: the
     caret is not a slow version of a press.

     Nothing here beats in time with anything else, on purpose. Synchronised
     ambient motion is what makes a page read as a loading screen.
  */
  --m-t-pulse: 1.25s;    /* the caret. all three of them. */
  --m-t-breath: 2.6s;    /* the status lamp */
  --m-t-sweep: 12s;      /* the panel sweep */
  --m-t-stagger: 45ms;   /* per-sibling delay in a run */

  /*
     Everything above is multiplied by this. At rest it is 1 and costs nothing;
     the design page's speed control sets it between 0.25 and 2 so that a 90ms
     press can actually be inspected.
  */
  --m-scale: 1;

  /* THREE EASINGS. */

  /* Arriving and settling. Fast out of the gate, long quiet tail. This was
     already the dominant curve in the file; it is now the standard. */
  --m-enter: cubic-bezier(0.22, 0.85, 0.25, 1);
  /* Leaving. Accelerates away and never decelerates -- nothing should look
     like it is being carefully placed somewhere on its way out. */
  --m-exit: cubic-bezier(0.4, 0, 1, 1);
  /* The signature moments, and only those. An extreme tail: it keeps moving
     long after it looks finished. Expensive. Use where the movement IS the
     point, not where something merely needs to change. */
  --m-emph: cubic-bezier(0.19, 1, 0.22, 1);

  /*
     `linear` is deliberately not a token. It is the ABSENCE of easing, and it
     is the right answer for colour and opacity, where a curve cannot be seen
     and only the duration reads. Anywhere a curve would show, one of the three
     above applies.
  */

  /* TWO TRAVEL DISTANCES. */

  /* The instrument's own scale: a lift, a nudge, a settle. Everything smaller
     derives from it -- a press is `calc(var(--m-near) / 3)`, a panel rise is
     `calc(var(--m-near) * 2)`. */
  --m-near: 3px;
  /* Entrance travel, and nothing else. The distance that reads as "this came
     from somewhere" rather than "this twitched". */
  --m-far: 20px;

}

/*
   THE SCALED TOKENS. These are what the rest of the file actually uses.

   Declared on `:root` AND on `.mlab` for a reason that is easy to get wrong: a
   custom property's `var()` is substituted against the element that DECLARES
   the property, not the element that uses it. With `--m-base` declared only on
   :root, setting `--m-scale` on a descendant changes nothing at all -- :root
   has already resolved `calc(1 * 180ms)` and handed the finished value down.

   So the speed control needs the arithmetic re-declared at the element it sets
   the scale on. The raw numbers still appear exactly once, above; this is the
   same expression evaluated at a second place, not a second set of values.
*/
:root, .mlab {
  --m-fast:    calc(var(--m-scale) * var(--m-t-fast));
  --m-base:    calc(var(--m-scale) * var(--m-t-base));
  --m-slow:    calc(var(--m-scale) * var(--m-t-slow));
  --m-pulse:   calc(var(--m-scale) * var(--m-t-pulse));
  --m-breath:  calc(var(--m-scale) * var(--m-t-breath));
  --m-sweep:   calc(var(--m-scale) * var(--m-t-sweep));
  --m-stagger: calc(var(--m-scale) * var(--m-t-stagger));
}

*, *::before, *::after { box-sizing: border-box; }

/* --- scrollbars ---------------------------------------------------------- */
/*
   One scrollbar everywhere, in the site's own palette. A default OS scrollbar
   beside a 46,888px amber-on-black wall is the one piece of chrome that
   announces it was never designed.

   Both syntaxes: the standard properties for Firefox, the ::-webkit- shadow
   parts for Chromium and Safari. Same measurements in both, so the page does
   not shift by a few pixels depending on the browser.

   scrollbar-gutter: stable reserves the track even when nothing overflows, so
   a short page and a long one have the same content width -- otherwise every
   navigation between them nudges the layout sideways.
*/
:root {
  --sb-size: 10px;
  --sb-track: #131314;
  --sb-thumb: var(--amber-low);
  --sb-thumb-hover: var(--amber-mid);
  scrollbar-width: thin;
  scrollbar-color: var(--sb-thumb) var(--sb-track);
}

html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }

::-webkit-scrollbar { width: var(--sb-size); height: var(--sb-size); }
::-webkit-scrollbar-track { background: var(--sb-track); }
::-webkit-scrollbar-thumb {
  background: var(--sb-thumb);
  border: 2px solid var(--sb-track);   /* inset, so the thumb reads as a bar */
}
::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); }
::-webkit-scrollbar-corner { background: var(--sb-track); }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-body);
  /* Mono at length needs air. This is what makes it readable rather than
     merely on-theme, and it is the whole cost of running one face. */
  line-height: 1.72;
  letter-spacing: -0.004em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Every number on this site is meant to be compared with the one above it. */
code, .fields, .ruler, table, .lbm, .stat__v, .rt { font-variant-numeric: tabular-nums; }

/* --- cursors, globally --------------------------------------------------- */
/*
   Four states everywhere, not just on the gallery: bone arrow at rest, amber
   arrow on anything interactive, an I-beam over text, a crosshair on the frame
   axis. The reticle and the lightbox chevrons are defined with their own
   components.

   Taking the default cursor means also restoring the I-beam, because the
   browser's automatic one only appears when `cursor` is unset. That is the
   whole reason the text rule below exists, and why it lists what it lists.
*/
body { cursor: var(--cur-default); }

a, label, summary, button, [role="button"], input[type="checkbox"],
.nav a, .tile--link, .jump a, .foot__set a, .minutes a, .views a {
  cursor: var(--cur-link);
}

p, li, dd, dt, td, th, blockquote, figcaption, code, pre,
h1, h2, h3, .lede, .note, .caveat, .prose, .rt__p, .lbq, .typeline {
  cursor: var(--cur-text);
}
/* ...but a heading or a label inside something clickable is not text to select. */
a p, a h1, a h2, a h3, a code, a .note,
.jump a *, .tile *, .foot__set a *, label * { cursor: inherit; }

/* The ruler is a measuring instrument. */
.ruler, .ruler__track, .ruler__axis, .span, .mark { cursor: var(--cur-cross); }
.ruler a, a.ruler__name { cursor: var(--cur-link); }

/* Faint horizon so the page does not read as a flat void. 43,200 frames wide. */
body::before {
  content: "";
  position: fixed;
  inset: 0 0 auto 0;
  height: 60vh;
  background: radial-gradient(120% 100% at 50% 0%, rgba(232,163,61,0.045), transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/*
   MAIN MUST NOT CARRY A Z-INDEX, and this is not tidiness.

   These three only need to sit above the horizon gradient, which is fixed at
   z-index 0. `position: relative` alone does that: a positioned element with
   `z-index: auto` paints after a z-index-0 pseudo-element of body in DOM
   order, and -- crucially -- does not create a stacking context.

   `main` used to be `z-index: 1`, which made it one. A stacking context is a
   ceiling: everything inside it is confined to the parent's layer no matter
   what it asks for. The lightbox asks for 60 and lives inside main, so it was
   pinned at layer 1 -- while the masthead, a SIBLING of main, sits at 40 and
   painted straight over the top of it.

   On desktop that hid nothing important by luck: the close control sits at
   top: 2.5rem, which clears the 3.4rem masthead by a few pixels. On a phone it
   moves to top: 0.55rem, landing dead centre of the nav -- so every tap on
   "close" hit a navigation link instead, and the top strip of the scrim was
   not dismissable either. There was no way out of the gallery but the browser
   back button.

   The lightbox is a full-screen overlay. It has to be able to say so.
*/
main { position: relative; }
.masthead, .foot { position: relative; z-index: 1; }

a { color: var(--amber); text-decoration-color: var(--amber-low); text-underline-offset: 0.2em; }
a:hover { text-decoration-color: var(--amber); }

:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 3px;
  border-radius: 1px;
}

.skip {
  position: absolute; left: -9999px;
  font-family: var(--mono); font-size: 0.82rem;
}
.skip:focus { left: var(--gut); top: 0.6rem; z-index: 50; background: var(--ground-3); padding: 0.5rem 0.8rem; }

/* --- masthead ------------------------------------------------------------ */

.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem 1.6rem;
  padding: 1.05rem var(--gut) 1rem;
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 0.78rem;
  /* The pass-02 wall is 46,888px tall. Navigation has to come with you. */
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--ground);
}
/* The burger is positioned against this. */
.masthead { position: sticky; }

.masthead__id { text-decoration: none; color: inherit; display: flex; align-items: baseline; gap: 0.6rem; }
.masthead__name { color: var(--lum-max); letter-spacing: -0.01em; }
.masthead__id:hover .masthead__name { color: var(--amber); }
.masthead__role { color: var(--dim); }
/* The facility's mark. Centred on its own, so the name and role keep their shared
   baseline beside it. 22px from a 64px file: sharp at 2x and 3x. */
.masthead__mark { width: 22px; height: 22px; align-self: center; flex: none; }
.masthead__role::before { content: "/ "; color: var(--rule); }

/*
   The mobile menu is a checkbox, not a script.

   The nav is six links; on a 375px screen they wrap to three rows and the
   masthead becomes a third of the viewport. A checkbox toggle needs no
   JavaScript, survives with scripting off like everything else here, and is
   focusable and operable from the keyboard because it is a real form control
   rather than a div pretending to be one.
*/
.nav-check { position: absolute; width: 1px; height: 1px; opacity: 0; }
.nav-burger { display: none; }

.nav { display: flex; flex-wrap: wrap; gap: 0 1.05rem; margin-left: auto; }
.nav a { color: var(--dim); text-decoration: none; }
.nav a:hover { color: var(--lum-max); }
.nav a[aria-current="page"] { color: var(--amber); }
.nav a[aria-current="page"]::before { content: "\00b7"; margin-right: 0.3em; }

.masthead__out { text-decoration: none; color: var(--dim); }
.masthead__out:hover { color: var(--amber); }

/* --- page shell ---------------------------------------------------------- */

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--gut); }
.wrap--narrow { max-width: 52rem; }

/*
   Section rhythm.

   This was 70px top and bottom, which put 141px of nothing between every pair
   of sections. On a page of short sections -- the design system page is eight
   of them -- the padding was the loudest element on screen. A hairline already
   marks the boundary; the space only has to let it breathe.
*/
.section { padding-block: clamp(1.9rem, 4vw, 3rem); }
.section + .section { border-top: 1px solid var(--rule-soft); }
.section--flush { padding-top: clamp(1.5rem, 3vw, 2.2rem); }

/* --- type ---------------------------------------------------------------- */

/* The micro-label. Uppercase mono, widely tracked, dim: the one device that
   makes a block read as an instrument panel rather than a document header. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 1.1rem;
}

.display {
  font-family: var(--mono);
  font-size: clamp(2.3rem, 8.2vw, 5rem);
  line-height: 0.93;
  letter-spacing: var(--track-display);
  font-weight: 600;
  color: var(--lum-max);
  margin: 0;
  text-wrap: balance;
  text-shadow: var(--glow-cool);
}

/* A terminal waiting for input, which is what this site is: a readout of a
   machine that is still running. Sized in em, so it scales with whatever
   heading it follows. */
.display::after,
h1.page-title::after {
  content: "";
  display: inline-block;
  width: 0.46em;
  height: 0.78em;
  margin-left: 0.14em;
  background: var(--amber);
  box-shadow: var(--glow-warm);
  animation: caret var(--m-pulse) steps(1) infinite;
  vertical-align: baseline;
}
@keyframes caret { 50% { opacity: 0; } }

/* The caret belongs where the typing happens. On the landing page that is the
   prompt line, not the title, so the title gives it up. */
.display--nocaret::after { display: none; }

/*
   Typed title. Characters are ALWAYS in the document and always occupy their
   space -- only opacity moves -- so there is no layout shift, no empty
   heading, and no largest-paint arriving a second late. The hidden state is
   applied by script, so a browser that never runs it sees the finished title.
*/
/* Applies to any typing header, not just the landing display. */
.ty { opacity: 1; }
.is-typing .ty { opacity: 0; }
/*
   NO TRANSITION, deliberately. A fade per character froze 18 of 22 letters at
   opacity 0 in a backgrounded tab: the transition started, never ticked, and
   the title was still half invisible after the typing "finished". A per-glyph
   fade is also the wrong gesture -- a terminal does not ease characters in, it
   puts them there. Instant means the end state is unconditional.
*/
.is-typing .ty.on { opacity: 1; }

/*
   While typing, the resting ::after caret gets out of the way and a real one
   rides the text instead. The pseudo-element sits after ALL the content, and
   every character holds its space from the first paint, so it would park at
   the end of the finished title while the typing happened mid-word.

   Absolutely positioned: moving it each tick must not reflow the line.
*/
.display, h1.page-title { position: relative; }
.is-typing::after { display: none !important; }

.ty-caret {
  position: absolute;
  width: 0.46em;
  height: 0.78em;
  background: var(--amber);
  box-shadow: var(--glow-warm);
  animation: caret var(--m-pulse) steps(1) infinite;
  pointer-events: none;
}

/*
   The live prompt line: the machine's input, in the machine's face, under the
   title rather than instead of it.
*/
/*
   ALL SIX PROMPTS OCCUPY ONE GRID CELL.

   Every line is in the document and every line is measured; only one is
   visible. The cell is therefore as tall as the longest prompt at the current
   width, always, and nothing below the hero can move -- not when the line
   changes, not as a line types itself out into a new wrap, not between
   refreshes.

   This replaced `min-height: calc(2 * 1.7em)`, which reserved two lines. Two
   was right on a desktop and wrong on a phone, where these prompts wrap to
   three and four -- and wrong by a different amount for each of the six, so
   the whole page shunted up and down as they cycled. Reserving a guess is the
   bug; reserving the real maximum is the fix, and the browser works it out
   rather than anybody measuring.

   `visibility: hidden` rather than `display: none` or `opacity: 0`: hidden
   keeps the box in the layout, which is the entire point, and unlike opacity
   it also takes the line out of the accessibility tree.
*/
.typeline {
  font-family: var(--mono);
  font-size: var(--t-data);
  line-height: 1.7;
  color: var(--dim);
  margin: 1.4rem 0 0;
  max-width: 44rem;
  display: grid;
}
.typeline__l {
  grid-area: 1 / 1;
  /* Top-aligned, so a short line sits where a long one starts rather than
     floating in the middle of the reserved box. */
  align-self: start;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.7rem;
}
.typeline__l:not(.is-on) { visibility: hidden; }
.typeline__f { color: var(--amber-mid); flex: none; }
.typeline__t { color: var(--text); }
.typeline__caret {
  display: inline-block;
  width: 0.5em;
  height: 1em;
  background: var(--amber);
  box-shadow: var(--glow-warm);
  animation: caret var(--m-pulse) steps(1) infinite;
  transform: translateY(0.12em);
  margin-left: -0.35rem;
}

h1.page-title, h2 { text-shadow: var(--glow-cool); }
.stat__v, .lb__id { text-shadow: var(--glow-cool); }

h1.page-title {
  font-family: var(--mono);
  font-size: var(--t-h1);
  line-height: 1.04;
  letter-spacing: var(--track-h1);
  font-weight: 600;
  color: var(--lum-max);
  margin: 0 0 0.75rem;
}

h2 {
  font-family: var(--mono);
  font-size: var(--t-h2);
  line-height: 1.2;
  letter-spacing: var(--track-h2);
  font-weight: 600;
  color: var(--lum-max);
  margin: 0 0 1.1rem;
}

/* A heading that follows content needs air; one that opens a block does not. */
:is(p, dl, table, ul, ol, .tablewrap, .panel, .legend, .ruler, .caveat, .stats, .prose) + h2 {
  margin-top: clamp(2.2rem, 5vw, 3.2rem);
}

h3 {
  font-family: var(--mono);
  font-size: var(--t-h3);
  letter-spacing: 0;
  font-weight: 600;
  color: var(--lum-max);
  margin: 2rem 0 0.7rem;
}

.lede {
  font-size: var(--t-lede);
  line-height: 1.75;
  color: var(--lum-max);
  max-width: var(--measure);
  margin: 0 0 1.5rem;
  text-wrap: pretty;
}
.lede--hero {
  margin-top: clamp(1.4rem, 3vw, 2.1rem);
  max-width: 44rem;
  margin-bottom: 0.9rem;
  /* A readout line, not a paragraph: it hangs off a rule the way a value
     hangs off a label everywhere else on the site. */
  border-left: 1px solid var(--amber-low);
  padding-left: clamp(0.9rem, 2vw, 1.4rem);
}
/*
   34rem, matching the statement directly above it rather than the 40rem it
   used to carry. Three stacked blocks at three different measures gave the
   hero three different right edges in no particular order -- and the widest
   measure of the three sat under the smallest type, which is the one pairing
   to avoid. Now the ruled line is deliberately wider and the two blocks under
   it agree.
*/
.lede--sub { font-size: var(--t-data); color: var(--dim); max-width: 34rem; letter-spacing: 0.01em; }

.prose { max-width: var(--measure); }
.prose p, .prose ul, .prose ol, .prose blockquote { margin: 0 0 1.05em; text-wrap: pretty; }
.prose > :last-child { margin-bottom: 0; }
.prose li { margin-bottom: 0.35em; }
.prose blockquote {
  border-left: 2px solid var(--amber-low);
  padding-left: 1.1rem;
  margin-left: 0;
  color: var(--dim);
}
.prose code, code { font-family: var(--mono); font-size: 0.86em; color: var(--lum-max); }
.prose h2 { margin-top: 2.2rem; }

.note {
  font-size: 0.82rem;
  line-height: 1.8;
  color: var(--dim);
  max-width: var(--measure);
}

/* --- the frame ruler ----------------------------------------------------- */
/*
   One shared coordinate system: frame 1 at the left edge, frame 43,200 at the
   right, which is also 00:00 to 30:00 at 24fps. Passes, runs, treatments,
   crashes and chain breaks are all placed on it, so spans are comparable
   between pages without reading a single number.
*/

.ruler { font-family: var(--mono); margin: 0 0 0.4rem; }

.ruler__axis {
  display: flex;
  justify-content: space-between;
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--dim);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.3rem;
  margin-bottom: 0.65rem;
}
.ruler__axis span { position: relative; }

.ruler__row {
  display: grid;
  grid-template-columns: 19ch 1fr minmax(8ch, auto);
  align-items: center;
  gap: 0 0.9rem;
  padding: 0.28rem 0;
  font-size: 0.74rem;
}
.ruler__row + .ruler__row { border-top: 1px solid var(--rule-soft); }

.ruler__name { color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.ruler__name { text-decoration: none; }
.ruler__row:hover .ruler__name, a.ruler__name:hover { color: var(--lum-max); }
.ruler__val { color: var(--dim); text-align: right; font-size: 0.7rem; white-space: nowrap; }

.ruler__track {
  position: relative;
  height: 0.85rem;
  background:
    repeating-linear-gradient(90deg, var(--rule-soft) 0 1px, transparent 1px 25%);
  border-bottom: 1px solid var(--rule-soft);
}

/*
   The default state is VISIBLE. That is the whole rule.

   This used to carry `animation: draw ... both`, which holds the from-state
   (scaleX(0)) until the animation runs -- so a bar only existed once its
   animation completed, and a tab that loaded in the background rendered every
   ruler permanently blank. Content must never depend on an animation
   finishing. Motion is added in the media query below, never subtracted here.
*/
.span {
  position: absolute;
  top: 0.14rem;
  bottom: 0.14rem;
  min-width: 2px;
  transform-origin: left center;
}

/* solid: covered by a verified manifest */
.span--verified { background: var(--amber); }
/* hollow: observed in the render log, not verified by a manifest */
.span--observed { border: 1px solid var(--amber-mid); background: var(--amber-wash); }
/* dashed: no record */
.span--none { border: 1px dashed var(--amber-low); }
/* Recovered from local staging after a crash, then verified. Tens of frames out of
   43,200, so it is given a floor width; the pages that use it say so. */
.span--rescued {
  min-width: 3px;
  background: var(--amber-wash);
  border-top: 1px solid var(--amber); border-bottom: 1px solid var(--amber);
  background-image: repeating-linear-gradient(45deg, var(--amber-mid) 0 1px, transparent 1px 4px);
}

@keyframes draw { from { transform: scaleX(0); } }

/*
   MOTION IS ADDITIVE. Everything below adds movement to content that is
   already on the page and already correct. Turn all of it off and the site
   loses nothing but its manners.

   Two tiers:
     1. Scroll-driven (`animation-timeline: view()`), where supported. A ruler
        draws when you arrive at it, not 0.55s after page load -- which is why
        the motion was invisible on every ruler below the fold.
     2. A time-based fallback for the first paint elsewhere.
*/
/*
   ONE-SHOT ON ARRIVAL, never scrubbed.

   An earlier version used `animation-timeline: view()`, which binds progress
   to scroll POSITION -- so scrolling back up ran everything in reverse and the
   rulers animated up and down under the reader's thumb. Wrong feel entirely: a
   reveal is an entrance, and an entrance happens once. The observer in
   static/js/site.js unobserves each element the moment it fires, so nothing
   ever replays.

   The hidden state is scoped to `.js-reveal`, a class the script adds to <html>
   only once it is running. No script, no hiding, complete page.
*/
@media (prefers-reduced-motion: no-preference) {

  .js-reveal [data-reveal],
  .js-reveal .panel,
  .js-reveal .caveat,
  .js-reveal .stats,
  .js-reveal .jump,
  .js-reveal .gcard,
  .js-reveal .stage,
  .js-reveal .filmstrip,
  .js-reveal .swatches,
  .js-reveal .strip,
  .js-reveal .player,
  .js-reveal .updown,
  .js-reveal .lede--hero {
    opacity: 0;
    transform: translateY(var(--m-far));
    /*
       800ms before the tokens, which is half a second of a paragraph being
       withheld from a reader who has already arrived at it. `--m-slow` is the
       arrival tier and this is an arrival; it does not get its own number.

       The delay is computed here from an INDEX the script sets, rather than
       the script handing over a millisecond value. That keeps the only motion
       constant out of the JavaScript, and it means the speed control scales
       the stagger along with everything else.
    */
    transition:
      opacity var(--m-slow) var(--m-emph) calc(var(--n, 0) * var(--m-stagger)),
      transform var(--m-slow) var(--m-emph) calc(var(--n, 0) * var(--m-stagger));
  }
  .js-reveal .in { opacity: 1; transform: none; }

  /*
     THE LAB IS EXEMPT, and this rule is not optional.

     site.js filters `.mlab` out of the observer, because a specimen of an
     effect must not fire the effect on scroll -- the replay button is what
     controls it. But excluding them from the thing that UN-HIDES while leaving
     them matched by the thing that HIDES left every caveat, stats block and
     ruler in the motion section sitting at opacity 0 forever, waiting for an
     observer that had been told to ignore them.

     This is the same failure the comment at the top of site.js describes, and
     it got in anyway: the hiding must never outlive the un-hiding. Anything
     that opts out of one has to opt out of both.
  */
  .js-reveal .mlab .caveat,
  .js-reveal .mlab .stats,
  .js-reveal .mlab .ruler,
  .js-reveal .mlab .panel,
  .js-reveal .mlab .jump,
  .js-reveal .mlab .strip,
  .js-reveal .mlab .filmstrip,
  .js-reveal .mlab [data-reveal] { opacity: 1; transform: none; }

  /*
     The ruler does not rise and does not fade. Its BARS draw, left to right,
     staggered 45ms a row -- the page rendering the way the work did. That was
     always the right motion; it only ever fired at the wrong time, 0.55s after
     page load, where every ruler below the fold finished before anyone
     arrived. Now it fires when the reader does.
  */
  .js-reveal .ruler .span { transform: scaleX(0); }
  .js-reveal .ruler.in .span {
    transform: none;
    transition: transform var(--m-slow) var(--m-enter)
                calc(var(--i, 0) * var(--m-stagger));
  }
}

.mark {
  position: absolute;
  top: -0.1rem; bottom: -0.1rem;
  width: 1px;
  background: var(--lum-max);
}
.mark--break { background: var(--amber-mid); top: 0.3rem; bottom: 0.3rem; }
.mark--stop::after {
  content: "";
  position: absolute; left: -2px; top: -2px;
  width: 5px; height: 5px;
  background: var(--lum-max);
}

.legend {
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.4rem;
  font-family: var(--mono); font-size: 0.68rem; color: var(--dim);
  margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--rule-soft);
}
.legend__item { display: flex; align-items: center; gap: 0.45rem; }
.legend__swatch { width: 1.6rem; height: 0.6rem; flex: none; }

/* --- fields (label / value pairs) ---------------------------------------- */

.fields {
  display: grid;
  grid-template-columns: minmax(8.5rem, 15rem) minmax(0, 1fr);
  gap: 0 1.4rem;
  margin: 0;
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: 1.55;
}
.fields .k {
  color: var(--dim);
  padding: 0.5rem 0;
  border-top: 1px solid var(--rule-soft);
  letter-spacing: 0.01em;
}
.fields .v {
  color: var(--text);
  margin: 0;
  padding: 0.5rem 0;
  border-top: 1px solid var(--rule-soft);
  overflow-wrap: anywhere;
}
.fields .v strong { color: var(--lum-max); font-weight: 500; }
.fields--tight { font-size: 0.74rem; }
:is(.prose, p, .note, .caveat, .tablewrap) + .fields { margin-top: 1.6rem; }
.fields .v em { font-style: normal; color: var(--dim); }

/* A value the record says was inferred rather than measured. */
.inferred {
  text-decoration: underline dotted var(--amber-mid);
  text-underline-offset: 0.28em;
  cursor: help;
}

.hash { color: var(--dim); font-size: 0.95em; }

/* --- panels -------------------------------------------------------------- */

.panel {
  background: var(--ground-2);
  border: 1px solid var(--rule);
  padding: clamp(1.1rem, 2.4vw, 1.7rem);
}
.panel + .panel { margin-top: 1rem; }
.panel__head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1rem;
  margin-bottom: 1rem;
}
.panel__title { font-family: var(--mono); font-size: 0.98rem; color: var(--lum-max); margin: 0; letter-spacing: -0.01em; }
.panel__title a { text-decoration: none; color: inherit; }
.panel__title a:hover { color: var(--amber); }

.status {
  font-family: var(--mono); font-size: 0.68rem;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--dim);
  border: 1px solid var(--rule); padding: 0.12rem 0.5rem;
  white-space: nowrap;
}
.status--live { color: var(--amber); border-color: var(--amber-low); }
.status--gone { color: var(--dim); border-style: dashed; }

/*
   A caveat carried straight out of the record. Never shrunk, never hidden.

   This was a translucent amber wash over near-black, which is just brown: the
   panel read muddy and flat and undid the contrast everything else works to
   keep. The fill is now a neutral raised ground and the amber is spent on
   EDGES instead -- corner brackets, a status lamp, and a slow sweep.

   The brackets are not generic sci-fi. The machine-vision overlay in the
   treatments draws corner brackets on every detection at bracket_len 56; the
   record says so. The site annotating its own gaps the way the work annotates
   what it sees is the aesthetic thread doing actual work.
*/
.annot,
.caveat,
.player__note {
  --annot-edge: var(--amber);
  position: relative;
  background:
    linear-gradient(var(--annot-edge) 0 0) 0 0 / 15px 1px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 0 0 / 1px 15px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 100% 0 / 15px 1px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 100% 0 / 1px 15px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 0 100% / 15px 1px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 0 100% / 1px 15px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 100% 100% / 15px 1px no-repeat,
    linear-gradient(var(--annot-edge) 0 0) 100% 100% / 1px 15px no-repeat,
    linear-gradient(180deg, rgba(232,163,61,0.05), transparent 38%),
    var(--ground-2);
  border: 1px solid var(--rule);
  padding: 1.05rem 1.25rem;
  margin: 1.3rem 0;
  font-size: 0.82rem;
  line-height: 1.8;
  color: var(--text);
  max-width: 46rem;
  overflow: hidden;
}

/* A sweep, once every twelve seconds. Slow enough to be noticed rather than
   watched, which is the line between a live instrument and a screensaver. */
.annot::after,
.caveat::after,
.player__note::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 34%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--amber), transparent);
  opacity: 0.85;
  animation: sweep var(--m-sweep) var(--m-enter) infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-110%); }
  55%  { transform: translateX(400%); }
  100% { transform: translateX(400%); }
}

.caveat__k {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: 0.5rem;
}
/* The status lamp. One per panel, breathing. */
.caveat__k::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  background: var(--amber);
  box-shadow: 0 0 8px var(--amber);
  /* The one keyword easing in the file was here (`ease-in-out`). A breath is a
     settle, so it takes the entrance curve like everything else. */
  animation: lamp var(--m-breath) var(--m-enter) infinite;
}
@keyframes lamp { 50% { opacity: 0.25; box-shadow: none; } }

.caveat p, .player__note { margin: 0; }
.caveat p + p { margin-top: 0.7rem; }

/* The note above the player is the same object, so it looks like one. */
.player__note { margin: 0 0 0.9rem; max-width: none; }

/* --- tables -------------------------------------------------------------- */

/* Horizontal only, for the same reason as the strips. */
.tablewrap { overflow-x: auto; overflow-y: hidden; margin: 1.2rem 0; }
table { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 0.76rem; }
th, td { text-align: left; padding: 0.5rem 1.1rem 0.5rem 0; border-top: 1px solid var(--rule-soft); vertical-align: top; white-space: nowrap; }
thead th {
  border-top: 0; border-bottom: 1px solid var(--rule);
  color: var(--dim); font-weight: 400;
  font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase;
  padding-bottom: 0.6rem;
}
td.num, th.num { text-align: right; padding-right: 1.1rem; }
tbody tr:hover td { color: var(--lum-max); }
td.wrap-cell { white-space: normal; min-width: 18rem; }
.t-dim { color: var(--dim); }
.t-on { color: var(--lum-max); }

/* label frequency: a table with a rule in it, not a chart */
.bar { position: relative; display: block; height: 0.5rem; background: var(--amber-low); min-width: 1px; }
.bar--key { background: var(--amber); }
.barcell { width: 55%; padding-top: 0.72rem; padding-right: 0; }

/* --- grids --------------------------------------------------------------- */

/*
   The single column below 60rem is minmax(0, 1fr), not the implicit `auto`.
   An `auto` track grows to its content's min-content width, so a table inside
   pushed the whole page past the screen instead of scrolling in its rail --
   book.html's contents table ran 495px wide on a 360px phone.
*/
.cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(1.4rem, 3vw, 2.6rem); }
/*
   A table beside a side heading is short by nature, so on a phone it wraps
   to fit rather than scrolling. Numbers stay on one line: "85-101" would
   otherwise break at its hyphen.
*/
@media (max-width: 59.99rem) {
  .cols--side td:not(.num) { white-space: normal; min-width: 0; }
}
@media (min-width: 60rem) {
  .cols--2 { grid-template-columns: 1fr 1fr; }
  /*
     CONTRACT: the first child holds a HEADING AND NOTHING ELSE. It is 13rem
     wide. Put prose, a field list or anything with its own internal columns in
     there and it will not wrap, it will collapse into a vertical stack of
     single words -- which is exactly what happened to the About page's "What
     this site is" section. Content goes in the second child. Two equal halves
     is `cols--2`.

     The label column was 20rem holding two words, which left a void the width
     of a paragraph beside every section. Narrower, and the heading now travels
     with its content instead of scrolling away from it -- so the column reads
     as a margin note rather than as space nobody filled.
  */
  .cols--side { grid-template-columns: minmax(0, 13rem) minmax(0, 1fr); }
  .cols--side > :first-child {
    position: sticky;
    top: calc(var(--masthead-h) + 1.6rem);
    align-self: start;
  }
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1px;
  background: var(--rule-soft);
  border: 1px solid var(--rule-soft);
  margin: 0;
}
.stat {
  background: var(--ground);
  padding: 0.9rem 1rem;
  position: relative;
  transition: background var(--m-base) linear;
}
/* A tick in the corner of every cell, the way a readout marks a live channel. */
.stat::before {
  content: "";
  position: absolute; top: 0; left: 0;
  width: 8px; height: 1px;
  background: var(--amber-mid);
}
/*
   The tick used to grow by animating its WIDTH from 8px to 100%, which is a
   layout property: every pointer entry reflowed the cell. This is the same
   effect drawn the only way it is allowed to be drawn -- a full-width rule
   scaled from its left edge, which the compositor does without touching
   layout or painting a pixel.

   Two elements rather than one because the resting 8px tick is a fixed
   length and the draw is proportional; scaling one into the other would make
   the resting tick's width depend on how wide the cell happened to be.
*/
.stat::after {
  content: "";
  position: absolute; top: 0; left: 0;
  width: 100%; height: 1px;
  background: var(--amber);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--m-base) var(--m-enter);
}
.stat:hover { background: var(--ground-3); }
.stat:hover::after { transform: scaleX(1); }
.stat__v { font-family: var(--mono); font-size: 1.28rem; color: var(--lum-max); letter-spacing: -0.03em; display: block; line-height: 1.1; }
.stat__k { font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.13em; text-transform: uppercase; color: var(--dim); display: block; margin-top: 0.4rem; }

/* --- player -------------------------------------------------------------- */

/*
   THE STATEMENT, set like the work's second channel.

   Channel 2 of the piece is the prompt rendered as monospaced text on black,
   frame-locked to the image. Setting the statement in that same face is not a
   costume: it is the one block on the site written in the work's own voice, so
   it gets the work's own typography. Looser leading and a shorter measure keep
   mono readable at length, which is the usual objection to it.
*/
.prose--statement {
  font-size: clamp(0.86rem, 0.82rem + 0.22vw, 0.98rem);
  line-height: 1.95;
  color: var(--lum-max);
  max-width: 34rem;
  text-shadow: var(--glow-cool);
}
.prose--statement p + p { margin-top: 1.5em; }
/*
   The statement's opening paragraph, in the landing hero rather than in a
   section of its own further down. It is the first thing that says what the
   work IS, so it goes where a reader lands instead of five sections below the
   fold. Its own narrower measure is kept -- 34rem against the hero line's
   44rem -- because the longer the text the shorter the line wants to be.
*/
.prose--hero { margin: clamp(1.4rem, 3vw, 2rem) 0 1.5rem; }
.prose--statement em { color: var(--amber); font-style: normal; }

/* --- landing: the stage and the filmstrip -------------------------------- */

/* Wider than the text column on purpose. An embedded player's resolution is
   chosen from its rendered size, so pixels on screen are pixels served. */
.stage { max-width: min(68rem, 100%); margin: 0 auto; }
.stage__note { max-width: 46rem; margin: 0.9rem auto 0; text-align: center; }

/*
   A scrollable run of stills across the whole pass. Full-bleed out of the
   text column on purpose -- it is the one element on the landing page allowed
   to break the measure, because the work is 43,200 frames wide and the page
   should say so before it says anything else.
*/
.filmstrip {
  margin-top: 0.4rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 4rem, #000 calc(100% - 4rem), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 4rem, #000 calc(100% - 4rem), transparent);
}
.filmstrip:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
.filmstrip__rail {
  display: flex;
  gap: 3px;
  /* Starts on the text column and bleeds off the right. Padding of just --gut
     put the first tile 360px left of every label on the page, which read as a
     misalignment rather than as a full-bleed. Aligned start, open end. */
  padding-left: max(var(--gut), calc((100% - var(--max)) / 2 + var(--gut)));
  padding-right: var(--gut);
  width: max-content;
}
.filmstrip .tile { flex: 0 0 auto; width: clamp(96px, 12vw, 150px); scroll-snap-align: center; }
.filmstrip .tile img { width: 100%; }

.player { max-width: 34rem; }
.stage .player { max-width: none; }
.player__frame {
  position: relative;
  aspect-ratio: var(--ar, 8 / 7);
  background: var(--ground-2);
  border: 1px solid var(--rule);
}
.player__frame video, .player__frame iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  border: 0;
  background: #000;
}
.player__empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(1rem, 3vw, 1.8rem);
  background-image:
    linear-gradient(90deg, transparent 0 calc(50% - 0.5px), var(--rule) calc(50% - 0.5px) calc(50% + 0.5px), transparent calc(50% + 0.5px));
}
.player__empty p { margin: 0; max-width: 26rem; color: var(--dim); font-size: 0.95rem; }
.player__empty .eyebrow { margin-bottom: 0.5rem; }
.player__meta {
  display: flex; flex-wrap: wrap; gap: 0.3rem 1.2rem;
  font-family: var(--mono); font-size: 0.68rem; color: var(--dim);
  margin-top: 0.6rem;
}

/* --- the gallery --------------------------------------------------------- */
/*
   Two geometries, measured rather than assumed. Pass stills come from a
   768x1344 channel and land at 514x900; treatment stills come from the
   1536x1344 side-by-side and land at 1028x900. build.py reads each file's
   real dimensions and writes them as width/height attributes, so the scroll
   height is correct before a single byte of image arrives.
*/

.wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(74px, 9vw, 116px), 1fr));
  gap: 2px;
}
/*
   Wide columns for stills that carry BOTH channels -- a paired view, or a
   treatment rendered from the 1536x1344 side-by-side. Those land at 1028x900
   against a pass channel's 514x900, so a column sized for the vertical still
   reduces them to a 118x103 sliver with two illegible halves. Roughly double
   the column, and the same amount of picture is actually visible.
*/
.wall--pair,
.wall--wide {
  grid-template-columns: repeat(auto-fill, minmax(clamp(150px, 18vw, 232px), 1fr));
  gap: 4px;
}

/*
   TILE STATES.

   A wall of 1,800 stills is a texture; the one under the pointer has to leave
   that texture to be legible. It lifts and scales while its neighbours recede,
   the way a focused card does on a television interface, and the reticle
   closes around it.

   All of it is CSS. Per-tile JavaScript across 1,800 elements would be a real
   cost for an effect that transform and opacity do on the compositor for
   free, and only the hovered tile ever animates.
*/
.tile {
  display: block;
  position: relative;
  text-decoration: none;
  background: var(--ground-2);
  line-height: 0;
  cursor: var(--cur-inspect);
  scroll-margin: 7rem 0 4rem;
  /*
     TRANSFORM ONLY, and the reason is arithmetic rather than taste.

     This used to transition `box-shadow` and `filter` as well. Shadow blur is
     a repaint on every frame of the transition, on the largest element on
     screen. The filter is worse: the neighbour-dim rule below matches every
     tile that is NOT hovered, so pointing at one tile on the gallery wall
     started a 240ms brightness/saturate transition on the other 1,799. That
     was the most expensive thing on the site, spent on an effect nobody was
     looking at -- by definition, since they were looking at the hovered one.

     Both now snap. The shadow appears with the lift and the neighbours drop
     the instant the pointer lands, which reads as a readout reacting rather
     than a carousel easing. Only the hovered tile animates, ever.
  */
  transition: transform var(--m-base) var(--m-enter);
}
.tile--link { cursor: pointer; }

.tile img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--ground-3);
  transition: opacity var(--m-base) linear, transform var(--m-slow) var(--m-enter);
}

/* Neighbours recede: dimmer and fractionally desaturated, so the focused tile
   is the only fully present thing on the wall. */
.wall:hover .tile:not(:hover),
.strip:hover .tile:not(:hover),
.filmstrip:hover .tile:not(:hover) { filter: brightness(0.6) saturate(0.75); }

.tile:hover,
.tile:focus-visible {
  transform: scale(1.16) translateY(calc(var(--m-near) * -1));
  z-index: 4;
  box-shadow:
    0 14px 34px rgba(0, 0, 0, 0.72),
    0 0 0 1px var(--amber-mid);
  filter: none;
}
/* The image drifts a little inside the frame while the tile is held open --
   a displacement, which is the operation this whole project is built on. */
.tile:hover img, .tile:focus-visible img { transform: scale(1.06); }

/* Press: it gives, briefly, and faster than it rose. */
.tile:active {
  transform: scale(1.06) translateY(0);
  transition-duration: var(--m-fast);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--amber);
}

/* The reticle: corner brackets, drawn the way the CV overlay draws them. */
.tile::before {
  content: "";
  position: absolute;
  inset: -4px;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--m-base) linear;
  background:
    linear-gradient(var(--amber) 0 0) 0 0 / 9px 1px no-repeat,
    linear-gradient(var(--amber) 0 0) 0 0 / 1px 9px no-repeat,
    linear-gradient(var(--amber) 0 0) 100% 0 / 9px 1px no-repeat,
    linear-gradient(var(--amber) 0 0) 100% 0 / 1px 9px no-repeat,
    linear-gradient(var(--amber) 0 0) 0 100% / 9px 1px no-repeat,
    linear-gradient(var(--amber) 0 0) 0 100% / 1px 9px no-repeat,
    linear-gradient(var(--amber) 0 0) 100% 100% / 9px 1px no-repeat,
    linear-gradient(var(--amber) 0 0) 100% 100% / 1px 9px no-repeat;
}
.tile:hover::before, .tile:focus-visible::before { opacity: 1; }

/*
   Closing the lightbox returns here, and says so loudly enough to find.

   A 1px outline is not findable on a wall of 1,800 tiles that all look alike.
   The tile you came back to holds its lift, keeps the reticle, brightens its
   frame number, and rings twice -- then settles to a permanent marker so the
   answer is still there a minute later.

   The ring is an animation and the outline is not, deliberately: if the
   animation never runs the marker is still correct, which is the same rule
   the ruler bars and the reveals follow.
*/
.tile:target {
  z-index: 3;
  transform: scale(1.1) translateY(calc(var(--m-near) * -1));
  /* Three arrivals long. The ring has to outlast the scroll that brought you
     here, or it has finished before you look up. Composed rather than a fourth
     duration -- the multiplier is the point being made. */
  animation: landed calc(var(--m-slow) * 3) var(--m-enter) 2;
}
.tile:target::before { opacity: 1; }            /* reticle stays closed on it */
.tile:target::after {
  content: "";
  position: absolute;
  inset: -3px;
  border: 1px solid var(--amber);
  box-shadow: 0 0 18px rgba(232, 163, 61, 0.35);
  pointer-events: none;
}
.tile:target .tile__f { color: var(--amber); }
@keyframes landed {
  from { box-shadow: 0 0 0 0 rgba(232, 163, 61, 0.6); }
  to   { box-shadow: 0 0 0 var(--ring, 18px) rgba(232, 163, 61, 0); }
}

/*
   And land it where it can be seen. Anchoring put the tile hard against the
   sticky masthead at the very top of the viewport, which on a 46,888px wall
   reads as "somewhere up there" rather than "here". A third of the way down
   puts it in the middle of attention, with its neighbours for context.
*/
.tile { scroll-margin-top: 32vh; scroll-margin-bottom: 24vh; }

/*
   The filename is the provenance. Frame number always, timecode on wider tiles.

   Positioned, on the tile's own background: the hover drift scales the image
   6%, and a transformed element paints over its unpositioned siblings, so the
   picture grew down over the caption and hid the one thing a tile must always
   show. Lifted above it, the caption stays legible and the image runs under
   its edge instead.
*/
.tile__f, .tile__t {
  display: block;
  position: relative;
  z-index: 1;
  background: inherit;
  font-family: var(--mono);
  font-size: 0.56rem;
  letter-spacing: 0.04em;
  line-height: 1.5;
  color: var(--dim);
  padding: 0.2rem 0.25rem 0;
}
.tile__t { display: none; padding-top: 0; color: var(--amber-mid); }
.tile:hover .tile__f, .tile--pair .tile__f { color: var(--lum-max); }
@media (min-width: 60rem) {
  .wall--pair .tile__t, .strip--wide .tile__t { display: block; }
}

.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--rule-soft); }
.pair img { width: 100%; height: auto; display: block; }
.tile--pair { background: var(--ground-2); }

/* A short horizontal sample: the home page and the gallery index cards. */
/*
   A scroll container clips -- that is what makes it scroll -- so a tile that
   scales up inside one gets its lift sliced off at the container's edge. It
   looked like the hover was masked, and it was.

   The fix is room, not overflow: vertical padding sized to hold the grown
   tile, and a gentler scale inside a rail than on an open wall. 8% of a 150px
   tile is 12px, 6px a side, plus a 2px lift -- inside the 16px of padding.
*/
/*
   Horizontal only. Setting overflow-x: auto forces overflow-y from `visible`
   to `auto` -- that is the spec, not a quirk -- so these rails quietly grew a
   vertical scrollbar the moment the padding and the hover scale gave them any
   height to spare. Pinning overflow-y: hidden is what makes them one-axis.

   It does not re-clip the hover, because the padding holds the grown tile
   INSIDE the box: a 150px tile at 1.08 grows about 21px, ~11px a side, plus a
   2px lift, against 16px of padding.
*/
.strip, .filmstrip { padding-block: 24px; overflow-y: hidden; }
.strip .tile:hover, .strip .tile:focus-visible,
.filmstrip .tile:hover, .filmstrip .tile:focus-visible {
  transform: scale(1.08) translateY(-2px);
}
/*
   AND THE LANDING MARKER NEEDS THE SAME TREATMENT.

   The hover was cut to 1.08 here so the grown tile stays inside the rail's
   padding. `:target` was left at 1.1 with a 3px lift, and it carries two
   things hover does not: an outline inset -3px, and a ring that spreads 18px.
   So closing a still and landing back on its tile drew a marker whose top edge
   fell outside the rail -- and a rail is overflow-y: hidden, so it was cut off
   cleanly, which reads as a rendering fault rather than a clipped effect.

   The arithmetic, for a 150px tile about 262px tall: at 1.08 it grows ~10.5px
   above, plus 2px of lift, plus the outline at ~3.2px scaled, plus a 6px ring
   at ~6.5px. That is ~22px against 24px of padding.

   The ring is a custom property rather than a second keyframe, so the wall
   keeps its full 18px and only the rails pay for their own clipping.
*/
.strip .tile:target, .filmstrip .tile:target {
  transform: scale(1.08) translateY(-2px);
  --ring: 6px;
}

.strip {
  display: grid;
  grid-auto-flow: column;
  /* A floor, so a wide still is never squeezed below legibility. The strip
     already scrolls, so exceeding the width costs nothing. */
  grid-auto-columns: minmax(var(--strip-min, 0px), 1fr);
  gap: 2px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.strip--wide-tiles { --strip-min: 168px; }
.strip--wide { gap: 3px; }
.strip .tile img { width: 100%; }

/* A frame a run produced, inside that run's panel. Provenance and picture in
   the same box, which is the point of putting it there. */
.run-still { margin: 1rem 0 0; }
.run-still a {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  text-decoration: none;
  font-family: var(--mono);
  font-size: var(--t-micro);
  color: var(--text);
}
.run-still img {
  width: 3.4rem;
  height: auto;
  flex: none;
  border: 1px solid var(--rule);
  background: var(--ground-3);
}
.run-still a:hover img { border-color: var(--amber); }
.run-still a:hover { color: var(--amber); }

.gcard { border-top: 1px solid var(--rule); padding: 1.6rem 0; }
.gcard:last-child { border-bottom: 1px solid var(--rule); }
.gcard__meta { font-family: var(--mono); font-size: 0.7rem; color: var(--dim); margin-left: auto; }
.gcard__foot {
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.2rem;
  font-family: var(--mono); font-size: 0.74rem; margin: 0.9rem 0 0;
}
.gcard__views { display: flex; flex-wrap: wrap; gap: 0 1rem; }
.gcard__foot a { text-decoration: none; }
.gcard__foot a:hover { text-decoration: underline; }
.gcard__foot > a:last-child { margin-left: auto; color: var(--dim); }
.gcard__foot > a:last-child:hover { color: var(--amber); }

.views {
  display: flex; flex-wrap: wrap; gap: 0 1.1rem;
  font-family: var(--mono); font-size: 0.78rem;
  margin: 0 0 1.6rem; padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--rule);
}
.views a { color: var(--dim); text-decoration: none; }
.views a:hover { color: var(--lum-max); }
.views [aria-current="page"] { color: var(--amber); }
.views [aria-current="page"]::before { content: "\00b7"; margin-right: 0.3em; }

/* --- the lightbox -------------------------------------------------------- */
/*
   :target, not JavaScript. The consequence is better than the constraint: the
   fragment IS the source frame number, so every still on this site is a URL
   that can be sent to someone, browser Back closes it, and the whole thing
   survives with scripting off. Hidden panels are display:none, so none of the
   4,090 lightbox images load until one is opened.
*/

.lb {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 60;
  padding: clamp(0.75rem, 3vw, 2.5rem);
}
/*
   Stepping between stills must not re-animate the whole overlay.

   The entrance used to live on :target, which meant every step faded the
   ENTIRE panel up from zero -- scrim included. Since each still is a separate
   element, the old one vanished and the new one dawned over black, so moving
   left or right strobed the whole screen. It read as a glitch, which on a site
   about a glitching render is the wrong kind of accident.

   The scrim is identical on every panel, so leaving it unanimated makes it
   read as one continuous surface the images change behind. The entrance is now
   opt-in via .lb-entering, added by site.js only when a panel opens from
   nothing -- never when stepping.
*/
.lb:target {
  display: grid;
  align-content: center;
}
.lb.lb-entering { animation: lb-in var(--m-base) var(--m-enter) both; }
@keyframes lb-in { from { opacity: 0; } }

.lb__scrim {
  position: fixed;
  inset: 0;
  background: rgba(6, 6, 7, 0.94);
  cursor: var(--cur-close);
}
/* The controls say which way they go before they are pressed. */
.lb__step:first-of-type { cursor: var(--cur-prev); }
.lb__step:last-of-type { cursor: var(--cur-next); }
.lb__close { cursor: var(--cur-close); }

.lb__inner {
  position: relative;
  display: grid;
  grid-template-columns: 2.75rem minmax(0, auto) minmax(0, 20rem) 2.75rem;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 2.5vw, 2rem);
  max-width: var(--max);
  margin: 0 auto;
  width: 100%;
}

.lb__fig { margin: 0; line-height: 0; }
/* Only on a real open. A 6px rise on every step is the flicker, not the polish. */
.lb.lb-entering .lb__fig { animation: lb-rise var(--m-base) var(--m-enter) both; }
/* Two nears. A panel is a bigger object than a link, so it travels further
   than a nudge and less far than an entrance. */
@keyframes lb-rise { from { transform: translateY(calc(var(--m-near) * 2)) scale(0.985); } }

/*
   Never upscale past native, never crop, never distort.

   `height: auto` is load-bearing, not tidiness. These images carry width and
   height attributes, so with only max-width and max-height set, the two
   clamps apply INDEPENDENTLY: a 1028x900 still hit max-width at 680 and
   max-height at 747 and rendered 680x747 -- squeezed to 0.910 from 1.142.
   A vertical still hid the bug completely, because only the height constraint
   ever bound. With both dimensions auto the browser reduces proportionally.
*/
.lb__fig img, .lb__fig video {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: min(82vh, 900px);
}
.lb__fig .pair { display: flex; gap: 1px; background: var(--rule); max-width: 100%; }
.lb__fig .pair img { width: auto; height: auto; max-width: 50%; max-height: min(82vh, 900px); }

.lb__meta { font-family: var(--mono); min-width: 0; }
.lb__id {
  font-size: clamp(1.5rem, 3.2vw, 2.1rem);
  letter-spacing: -0.04em;
  color: var(--lum-max);
  margin: 0;
  line-height: 1;
}
.lb__sub { font-size: 0.74rem; color: var(--dim); margin: 0.45rem 0 1.2rem; }
.lbm {
  display: grid;
  grid-template-columns: minmax(6rem, 8rem) minmax(0, 1fr);
  gap: 0 1.1rem;
  margin: 0;
  font-size: 0.74rem;
  line-height: 1.5;
}
.lbm dt { color: var(--dim); padding: 0.45rem 0; border-top: 1px solid var(--rule-soft); }
.lbm dd { color: var(--text); margin: 0; padding: 0.45rem 0; border-top: 1px solid var(--rule-soft); overflow-wrap: anywhere; }
.lbm dd strong { color: var(--lum-max); font-weight: 500; }

/* The prompt line. Long enough that a narrow value column would shred it, so
   the row spans and the label sits above the text. */
.lbm__w { grid-column: 1 / -1; }
dt.lbm__w {
  padding-bottom: 0;
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
dd.lbm__w { border-top: 0; padding-top: 0.2rem; }
.lbq { color: var(--lum-max); }
.lb__pos {
  font-size: 0.72rem; color: var(--lum-max);
  margin: 1.2rem 0 0; padding-top: 0.7rem;
  border-top: 1px solid var(--rule);
}

/*
   Only on the readout: the two strings side by side. This is the one place the
   site can show what the audience read and what the model was told at the same
   time, on the same frame, and that difference is the piece's argument.
*/
.lbp { margin-top: 1.1rem; }
.lbp__k {
  display: block;
  font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--dim); margin-top: 0.9rem;
}
.lbp__t {
  font-size: 0.78rem; line-height: 1.5; color: var(--text);
  margin: 0.3rem 0 0;
}
.lbp__t--sent { color: var(--dim); }
.lbp__x { color: var(--amber); }

/* Closing the lightbox returns to the row it was opened from. */
.rt tr:target td { background: var(--amber-wash); color: var(--lum-max); }
.rt tr:target .rt__f { color: var(--amber); }

.lb__step {
  display: grid;
  place-items: center;
  align-self: stretch;
  font-family: var(--mono);
  font-size: 1.9rem;
  line-height: 1;
  color: var(--dim);
  text-decoration: none;
  border: 1px solid transparent;
  transition: color var(--m-fast) linear, border-color var(--m-fast) linear;
}
.lb__step:hover { color: var(--lum-max); border-color: var(--rule); }

/* Leaving is the one red action. Four states, because a control this important
   should answer every kind of attention: rest, pointed at, focused, pressed. */
.lb__close {
  position: absolute;
  top: clamp(0.75rem, 3vw, 2.5rem);
  right: clamp(0.75rem, 3vw, 2.5rem);
  /*
     AN ICON BUTTON. A square, and a cross drawn inside it.

     It used to read "CLOSE x" -- a word, a gap, and a multiplication sign --
     laid out with `display: grid`, which made the bare text node and the
     <span> into two separate grid ITEMS and stacked them in two rows inside a
     box measured for one. It looked like a form control dropped on a
     photograph.

     The cross is now GEOMETRY, not a glyph: two hairlines crossed, which is
     how the mobile menu builds its X and how the close cursor is drawn. Same
     mark in all three places. A typographic x would be a fourth thing that
     nearly matches.

     Square, 44px: the touch-target floor in both Apple's guidance and WCAG,
     and this is the control someone reaches for when they want out.
  */
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  color: var(--exit);
  text-decoration: none;
  border: 1px solid var(--exit-dim);
  background: rgba(209, 90, 78, 0.08);
  transition: background var(--m-fast) linear, border-color var(--m-fast) linear,
              color var(--m-fast) linear, transform var(--m-fast) var(--m-enter);
}
/* The cross itself. `currentColor`, so it inherits every state the button
   already defines rather than restating four colours. */
.lb__close span {
  position: relative;
  display: block;
  width: 0.95rem;
  height: 0.95rem;
}
.lb__close span::before,
.lb__close span::after {
  content: "";
  position: absolute;
  /* Half the stroke back up. `top: 50%` puts the line's TOP edge on the centre
     line, which leaves the mark sitting half a pixel low -- invisible on one
     rule and not on two crossing ones. */
  top: calc(50% - 0.5px);
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
}
.lb__close span::before { transform: rotate(45deg); }
.lb__close span::after { transform: rotate(-45deg); }
.lb__close:hover {
  color: var(--lum-max);
  border-color: var(--exit);
  background: rgba(209, 90, 78, 0.22);
}
.lb__close:focus-visible { outline: 2px solid var(--exit); outline-offset: 2px; }
.lb__close:active {
  background: var(--exit);
  color: var(--ground);
  border-color: var(--exit);
  transform: scale(0.96);
}

.lb__step-f { display: none; }

/* Desktop has the keyboard, the scrim and a control it can reach; it does not
   need to be told about a gesture it cannot make. Turned on inside the mobile
   query below -- declared here so the media rule is the one that wins. */
.lb__hint { display: none; }

/*
   MOBILE LIGHTBOX: the controls do not scroll.

   They used to sit in a grid row under the metadata, which on a phone put them
   below the fold behind a prompt and five provenance rows. Worse, stepping to
   the next still is a fragment navigation, so the page jumped to the top of
   the new panel and the controls were out of view AGAIN -- every single step
   cost a scroll down and back. That is the opposite of how anyone flicks
   through images.

   Prev and next are now pinned to the bottom edge, in the thumb zone, always
   reachable: two 50% targets over 3.5rem tall, clear of the iPhone home
   indicator via env(safe-area-inset-bottom). Close is pinned top-right, clear
   of the notch. Each control names the frame it goes to, so stepping through
   a sampled gallery shows the stride. Swiping works too -- see site.js.
*/
@media (max-width: 58rem) {
  .lb { padding: 0; }
  .lb:target {
    align-content: start;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  .lb__inner {
    display: block;
    padding: calc(env(safe-area-inset-top, 0px) + 3.6rem)
             clamp(0.9rem, 4vw, 1.4rem)
             calc(env(safe-area-inset-bottom, 0px) + 4.6rem);
  }
  .lb__fig { margin: 0 auto 1.2rem; }
  .lb__fig img, .lb__fig video, .lb__fig .pair img { max-height: 50vh; margin-inline: auto; }

  .lb__step {
    position: fixed;
    bottom: 0;
    width: 50%;
    min-height: calc(env(safe-area-inset-bottom, 0px) + 3.5rem);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    background: var(--ground-2);
    border-top: 1px solid var(--rule);
    font-size: 1.5rem;
    color: var(--lum-max);
    z-index: 2;
  }
  .lb__step:first-of-type { left: 0; border-right: 1px solid var(--rule); }
  .lb__step:last-of-type { right: 0; }
  .lb__step:active { background: var(--ground-3); color: var(--amber); }
  .lb__step-f {
    display: inline;
    font-size: var(--t-micro);
    letter-spacing: 0.06em;
    color: var(--dim);
  }

  /*
     Close is pinned top-right and, since the stacking fix above, is genuinely
     on top rather than underneath the site header.

     env(safe-area-inset-top) resolves to 0 here and that is correct, not a
     bug: the insets only become non-zero with `viewport-fit=cover` in the
     viewport meta, which this site deliberately does not set. Without it
     Safari already insets the layout viewport below the notch, so there is
     nothing to avoid. The env() is kept as the fallback that would do the
     right thing if that ever changes -- adding viewport-fit=cover is what
     would CREATE the problem it solves.
  */
  .lb__close {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0px) + 0.75rem);
    right: clamp(0.9rem, 4vw, 1.4rem);
    z-index: 3;
    /*
       Anchoring and ground only. The base rule already makes this a 44px
       square with the cross centred in it, and the size must not be restated
       here -- an earlier version left `padding: 0 1.1rem` on this rule, which
       squeezed the 44px box down to an 8.8px content area and pushed the cross
       off centre.
    */
    background: var(--ground-2);
    border: 1px solid var(--exit-dim);
  }
  /*
     And a third way out, because the top-right corner of a 6.3-inch phone is
     the one place a thumb cannot reach. Prev and next stay where they are --
     they are well placed and heavily used -- so the dismiss gesture goes to
     the swipe instead: pull the panel down and it closes. See site.js.
  */
  .lb__hint {
    display: block;
    text-align: center;
    font-size: var(--t-micro);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--dim);
    margin: 0 0 1rem;
  }
}

/*
   Coarse pointers get none of it. There is no hover on a touchscreen, so a
   hover state there fires on tap and reads as lag; and a custom cursor is
   meaningless without a pointer.
*/
@media (hover: none) {
  .wall:hover .tile:not(:hover),
  .strip:hover .tile:not(:hover),
  .filmstrip:hover .tile:not(:hover) { filter: none; }
  /* A custom cursor is meaningless without a pointer -- hand the whole system
     back to the platform rather than shipping invisible decoration. */
  body, a, label, summary, button, [role="button"],
  p, li, dd, dt, td, th, code, pre, h1, h2, h3,
  .ruler, .ruler__track, .span, .mark,
  .tile, .tile--link, .lb__scrim, .lb__step, .lb__close { cursor: auto; }
  a, label, summary, button, [role="button"], .tile { cursor: pointer; }
  .tile:hover { transform: none; box-shadow: none; }
  .tile:hover img { transform: none; }
  .tile:hover::before { opacity: 0; }
  .tile:active { transform: scale(0.97); transition-duration: var(--m-fast); }
  .tile__t { display: block; }
}

/* --- the prompt readout -------------------------------------------------- */
/*
   A spreadsheet of the work: one row per frame, paginated by minute. The
   closest thing on the site to a terminal, which is the right register --
   this is the text channel, and the text channel is a monospaced render of
   the same lines.
*/

/*
   NOT a scroll container, deliberately.

   overflow-x: auto here made this box the sticky containing block, so the
   sticky header resolved against the table instead of the viewport: top:3.4rem
   pushed it 3.4rem DOWN from the table's own top, leaving row 1 stranded above
   the header and row 2 hidden behind it. The readout wraps its prompt column
   instead of scrolling sideways, so it does not need the overflow at all.
*/
/*
   BOTH AXES. `overflow-x: visible` alone was not enough and this regressed
   once already because of it.

   The base .tablewrap carries `overflow-y: hidden`, added so horizontal rails
   could not grow a vertical scrollbar. Overriding only overflow-x left this
   box at `visible` / `hidden` -- and a computed `visible` beside any
   non-visible value becomes `auto`. The box was a scroll container again, the
   sticky header resolved against it again, and row 1 was stranded above the
   header again.

   The rule is symmetric and it is the same one written up for the rails: the
   two axes are not independent. If a box must not scroll, say so on both.
*/
.tablewrap--rt { overflow: visible; }

.rt { font-size: 0.74rem; }
/* Sits directly under the sticky masthead. 1,440 rows need their column names. */
.rt th { position: sticky; top: var(--masthead-h); background: var(--ground); z-index: 5; }
.rt td { padding: 0.3rem 1.1rem 0.3rem 0; border-top: 1px solid transparent; }
.rt tbody tr:hover td { background: var(--ground-2); color: var(--lum-max); }
.rt__f { color: var(--dim); }
.rt tbody tr:hover .rt__f { color: var(--amber); }
.rt__p { white-space: normal; min-width: 22rem; color: var(--text); }
/* The readout row you came back from: same idea, mid-screen rather than jammed
   under the masthead, and marked so it is findable in 1,440 rows. */
.rt tr { scroll-margin-top: 34vh; scroll-margin-bottom: 24vh; }

/* Narrow: the masthead wraps and gets taller, and the table has to shed a
   column rather than scroll sideways. */
/* Tied to the nav collapsing, so it moves with that breakpoint, not the
   table one: once the links are behind a burger the masthead is one line. */
@media (max-width: 59.99rem) {
  :root { --masthead-h: 3.5rem; }
}

/* A table concern, independent of the nav: below this the readout sheds its
   position column rather than scrolling sideways. */
@media (max-width: 48rem) {
  .rt th:nth-child(4), .rt td:nth-child(4) { display: none; }
  .rt__p { min-width: 0; }
}

/* A rule where the viewpoint era changes: the 72/36/72/36/48/36/24/36 rhythm
   is the schedule's actual structure, so it is drawn rather than described. */
.rt tr.era td { border-top: 1px solid var(--rule); }
/* Full denoise: the feedback chain is cut and the frame starts clean. */
.rt tr.cut td { color: var(--lum-max); }
.rt tr.cut .rt__f::after { content: " \00b7 cut"; color: var(--amber); }

.minutes {
  display: flex; flex-wrap: wrap; gap: 0.3rem;
  font-family: var(--mono); font-size: 0.74rem; margin: 0.9rem 0 0;
}
.minutes a, .minutes [aria-current] {
  display: grid; place-items: center;
  min-width: 2.1rem; padding: 0.3rem 0.35rem;
  border: 1px solid var(--rule);
  text-decoration: none; color: var(--dim);
}
.minutes a:hover { border-color: var(--amber-low); color: var(--lum-max); }
.minutes [aria-current] { border-color: var(--amber); color: var(--amber); }
.minutes--strip { margin-top: 1.4rem; }

/* --- design system page -------------------------------------------------- */

.swatches {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1px;
  background: var(--rule-soft);
  border: 1px solid var(--rule-soft);
}
.sw {
  display: grid;
  grid-template-columns: 2.4rem minmax(0, 1fr);
  grid-template-areas: "chip name" "chip hex" "chip what";
  align-items: center;
  gap: 0 0.85rem;
  background: var(--ground);
  padding: 0.75rem 0.9rem;
  font-size: var(--t-micro);
}
.sw__chip {
  grid-area: chip;
  align-self: stretch;
  min-height: 3.1rem;
  border: 1px solid var(--rule);
}
.sw__n { grid-area: name; color: var(--lum-max); }
.sw__h { grid-area: hex; color: var(--amber-mid); }
.sw__w { grid-area: what; color: var(--dim); line-height: 1.5; }

/* --- links list ---------------------------------------------------------- */

.jump { display: grid; gap: 1px; background: var(--rule-soft); border-block: 1px solid var(--rule-soft); }
.jump a {
  background: var(--ground);
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline; gap: 1rem;
  padding: 0.85rem 0.2rem;
  text-decoration: none;
  font-family: var(--mono); font-size: 0.82rem;
  color: var(--lum-max);
}
.jump a { position: relative; padding-left: 1.6rem; transition: background var(--m-fast) linear; }
/* A caret that advances on hover. The list reads as a command menu. */
.jump a::before {
  content: "\203A";
  position: absolute; left: 0.35rem;
  color: var(--amber-low);
  transition: transform var(--m-base) var(--m-enter), color var(--m-base) linear;
}
.jump a:hover { background: var(--ground-3); color: var(--amber); }
.jump a:hover::before { transform: translateX(calc(var(--m-near) * 1.5)); color: var(--amber); }
.jump .jump__d { color: var(--dim); font-size: 0.72rem; text-align: right; }
.jump a:hover .jump__d { color: var(--dim); }

/* --- footer -------------------------------------------------------------- */

.foot {
  border-top: 1px solid var(--rule);
  margin-top: clamp(3rem, 8vw, 6rem);
  padding: clamp(2.2rem, 5vw, 3.4rem) var(--gut) clamp(1.4rem, 3vw, 2rem);
}
.foot__top {
  max-width: var(--max); margin: 0 auto;
  display: grid; gap: clamp(2rem, 5vw, 3.5rem);
}
@media (min-width: 58rem) { .foot__top { grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); } }

.foot__lede { margin: 0 0 0.9rem; color: var(--dim); max-width: 24rem; font-size: 0.92rem; }
.foot__big {
  font-family: var(--mono);
  font-size: clamp(1.1rem, 2.6vw, 1.55rem);
  letter-spacing: -0.03em;
  text-decoration: none;
}
.foot__big:hover { text-decoration: underline; text-decoration-color: var(--amber); }

.foot__nav { display: grid; gap: 1.8rem 2.5rem; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); }
.foot__set { display: flex; flex-direction: column; gap: 0.35rem; align-items: start; }
.foot__k {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--dim); margin: 0 0 0.35rem;
}
.foot__set a {
  font-family: var(--mono); font-size: var(--t-data);
  color: var(--text); text-decoration: none;
}
.foot__set a:hover { color: var(--amber); }

.foot__bar {
  max-width: var(--max); margin: clamp(2rem, 5vw, 3rem) auto 0;
  padding-top: 1.2rem; border-top: 1px solid var(--rule-soft);
  display: grid; gap: 1rem;
}
@media (min-width: 58rem) { .foot__bar { grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); gap: 2.5rem; } }
.foot__meta {
  margin: 0; font-family: var(--mono); font-size: var(--t-micro);
  display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.8rem;
}
.foot__meta dt { color: var(--dim); }
.foot__meta dd { color: var(--text); margin: 0; overflow-wrap: anywhere; }
.foot__note { font-size: 0.82rem; color: var(--dim); margin: 0; max-width: 42rem; }

/* --- back to top --------------------------------------------------------- */
/*
   The pass-02 wall is 46,888px tall and the prompt readout is 1,440 rows. Both
   are meant to be scrolled a long way, which makes getting back a real cost.

   A plain anchor, so it works even if the click handler never runs, hidden
   until there is something to go back from. It is suppressed entirely while a
   lightbox is open, because on a phone it would sit on top of the panel's own
   fixed prev/next bar.
*/
.to-top {
  position: fixed;
  right: clamp(0.9rem, 3vw, 1.6rem);
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(0.9rem, 3vw, 1.6rem));
  z-index: 35;
  display: grid;
  place-items: center;
  width: 2.9rem;
  height: 2.9rem;
  font-family: var(--mono);
  font-size: 1.05rem;
  line-height: 1;
  color: var(--dim);
  text-decoration: none;
  background: var(--ground-2);
  border: 1px solid var(--rule);
  opacity: 0;
  pointer-events: none;
  transform: translateY(calc(var(--m-near) * 2));
  transition: opacity var(--m-base) linear, transform var(--m-base) var(--m-enter),
              color var(--m-fast) linear, border-color var(--m-fast) linear;
}
.to-top.is-visible { opacity: 1; pointer-events: auto; transform: none; }
.to-top:hover { color: var(--amber); border-color: var(--amber-low); }
.to-top:active { background: var(--ground-3); }
/* Never over the lightbox's own fixed controls. */
.lb-open .to-top { display: none; }

/* --- states, everywhere -------------------------------------------------- */
/*
   Most controls had a rest and a hover and nothing else. A control should
   answer every kind of attention it can receive: pointed at, reached by
   keyboard, and pressed. The press is the one that was missing everywhere, and
   it is the one that makes an interface feel like it is made of objects.

   Transforms are kept tiny -- 1px, 0.97 -- because this is feedback, not
   animation, and it has to resolve faster than the eye asks about it.
*/
/*
   The press travel is one third of a near -- 1px. It is not a movement, it is
   an acknowledgement, and anything further reads as the control being knocked
   rather than pressed.
*/
.nav a, .foot__set a, .jump a, .minutes a, .views a,
.masthead__id, .masthead__out, .ruler__name, .updown a,
.gcard__foot a, .foot__big, .run-still a {
  transition: color var(--m-fast) linear, background var(--m-fast) linear,
              border-color var(--m-fast) linear, transform var(--m-fast) linear;
}
.nav a:active, .foot__set a:active, .views a:active,
.masthead__id:active, .masthead__out:active, .updown a:active,
.gcard__foot a:active, .foot__big:active, .run-still a:active {
  color: var(--amber);
  transform: translateY(calc(var(--m-near) / 3));
}
.jump a:active { background: var(--ground-2); transform: translateY(calc(var(--m-near) / 3)); }
.minutes a:active {
  border-color: var(--amber); color: var(--amber);
  transform: translateY(calc(var(--m-near) / 3));
}

/*
   Keyboard reach is never implied by a hover rule.

   Four controls reached this list late -- .foot__big, .masthead__out,
   .run-still a and .gcard__foot a each had a hover and nothing else. They were
   never unreachable: the global :focus-visible at the top of this file gave
   them a ring. They just did not do the colour shift every one of their
   siblings does, so tabbing across the footer lit some links up and merely
   outlined others. Inconsistent, which on a keyboard is the same as unfinished.
*/
.nav a:focus-visible, .foot__set a:focus-visible, .jump a:focus-visible,
.minutes a:focus-visible, .views a:focus-visible, .updown a:focus-visible,
.ruler__name:focus-visible, .to-top:focus-visible, .foot__big:focus-visible,
.masthead__out:focus-visible, .run-still a:focus-visible,
.gcard__foot a:focus-visible, .masthead__id:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
  color: var(--lum-max);
}
/* The still's frame takes the ring's colour, the same as on hover. */
.run-still a:focus-visible img { border-color: var(--amber); }

.lb__step {
  transition: background var(--m-fast) linear, color var(--m-fast) linear,
              transform var(--m-base) var(--m-enter);
}
.lb__step:hover { background: var(--ground-3); color: var(--amber); }

/*
   THE CHEVRON LEANS THE WAY IT WILL TAKE YOU.

   A cursor IMAGE cannot be animated -- `cursor:` takes a static bitmap, and no
   browser runs SMIL or CSS animation inside one, so the most a cursor can do
   is cut between frames on a state change. The motion has to live on the
   control instead, which is better anyway: a DOM element that chases the
   pointer always trails it, and lag on a site claiming to be an instrument
   reads as the instrument being slow.

   So the affordance moves and the cursor stays crisp. One --m-near, the same
   distance every other nudge on the site uses.

   Pointer only: on a touchscreen `:hover` fires on tap and would shunt the
   fixed bottom bar sideways as the panel changed.
*/
@media (hover: hover) {
  .lb__step:first-of-type:hover { transform: translateX(calc(var(--m-near) * -1)); }
  .lb__step:last-of-type:hover { transform: translateX(var(--m-near)); }
}
.lb__step:focus-visible { outline: 2px solid var(--amber); outline-offset: -3px; color: var(--amber); }
.lb__step:active { background: var(--amber-low); color: var(--ground); }

.to-top:active { transform: scale(0.94); background: var(--ground-3); }

/* The scrim is a dismiss target, so it reads as one when pressed. */
.lb__scrim:active { background: rgba(6, 6, 7, 0.98); }

/* --- pagination between record pages ------------------------------------- */

.updown {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem;
  font-family: var(--mono); font-size: 0.78rem;
  margin-top: 2.6rem; padding-top: 1rem; border-top: 1px solid var(--rule);
}
.updown a { text-decoration: none; }
.updown a:hover { text-decoration: underline; }

/* --- the motion lab ------------------------------------------------------ */
/*
   The motion system, shown in itself, on the design page.

   Same rule as the rest of that page: every specimen is the REAL component
   with the real classes. The caret specimen is `.typeline__caret`. The lamp
   and sweep specimens are a real `.caveat` with the other effect switched off
   so each is seen alone. The ruler draws using the shipped rule. Nothing here
   is a picture of an effect, so nothing here can drift from one.

   Three controls, and each one earns its place:
     - REPLAY, because half of these are one-shot and reviewing an entrance you
       have to reload the page to see again is not reviewing it.
     - SPEED, because a 90ms press cannot be judged at 90ms. It sets --m-scale,
       which every token multiplies by, so one property slows the whole system.
     - REDUCED MOTION, because the fallback is a design decision and it should
       be reviewable without changing an OS setting. It is not a mock: it sets
       the same token values the real media query sets.
*/

.mlab { margin: 1.4rem 0 0; }

/* The checkbox is the mechanism, not a control -- the label below is what gets
   pointed at. Kept in the flow (not display:none) so it stays focusable. */
.mlab__rm {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}

.mlab__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.8rem 1.8rem;
  padding: 0.75rem 0.9rem;
  background: var(--ground-2);
  border: 1px solid var(--rule);
  /* The controls stay reachable while the specimens scroll past them.
     Hangs off the masthead like everything else that sticks. */
  position: sticky;
  top: var(--masthead-h);
  z-index: 6;
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--dim);
}
.mlab__ctl { display: flex; align-items: center; gap: 0.6rem; }

/* Replay and speed do nothing without scripting, so they are not shown
   without it. A dead control is worse than a missing one. */
.mlab__js { display: none; }
.mlab--js .mlab__js { display: flex; }

.mlab__btn {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--dim);
  background: var(--ground);
  border: 1px solid var(--rule);
  padding: 0.35rem 0.7rem;
  cursor: var(--cur-link);
  transition: color var(--m-fast) linear, border-color var(--m-fast) linear,
              background var(--m-fast) linear, transform var(--m-fast) linear;
}
.mlab__btn:hover { color: var(--amber); border-color: var(--amber-low); }
.mlab__btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; color: var(--lum-max); }
.mlab__btn:active { background: var(--ground-3); color: var(--amber); transform: translateY(calc(var(--m-near) / 3)); }

.mlab__speed { accent-color: var(--amber); width: 9rem; cursor: var(--cur-link); }
.mlab__speed:focus-visible { outline: 2px solid var(--amber); outline-offset: 3px; }
.mlab__out { color: var(--amber); font-variant-numeric: tabular-nums; min-width: 3.2em; }

/* The reduced-motion switch: a lamp that goes out. */
.mlab__sw { display: flex; align-items: center; gap: 0.55rem; cursor: var(--cur-link); }
.mlab__swbox {
  width: 1.9rem; height: 0.85rem;
  background: var(--ground);
  border: 1px solid var(--rule);
  position: relative;
  flex: none;
}
.mlab__swbox::after {
  content: "";
  position: absolute;
  top: 1px; left: 1px; bottom: 1px;
  width: 0.75rem;
  background: var(--amber-low);
  transition: transform var(--m-fast) var(--m-enter), background var(--m-fast) linear;
}
.mlab__rm:checked ~ .mlab__bar .mlab__swbox::after {
  transform: translateX(0.9rem);
  background: var(--amber);
}
.mlab__rm:focus-visible ~ .mlab__bar .mlab__swbox { outline: 2px solid var(--amber); outline-offset: 2px; }
.mlab__rm:checked ~ .mlab__bar .mlab__sw { color: var(--amber); }

/*
   THE FALLBACK PREVIEW.

   The same values the real media query sets, applied to the grid only. It is
   the actual fallback rather than a demonstration of one -- if the tokens
   change, this changes with them, and there is no second definition to keep
   in sync.
*/
.mlab__rm:checked ~ .mlab__grid {
  --m-fast: 1ms;
  --m-base: 1ms;
  --m-slow: 1ms;
  --m-near: 0px;
  --m-far: 0px;
  --m-stagger: 0ms;
}
/* Ambient loops stop rather than run fast: an infinite animation at one
   millisecond is a strobe, which is what the preference exists to prevent. */
.mlab__rm:checked ~ .mlab__grid .typeline__caret,
.mlab__rm:checked ~ .mlab__grid .caveat__k::before { animation: none; opacity: 1; }
.mlab__rm:checked ~ .mlab__grid .caveat::after { animation: none; opacity: 0; }

.mlab__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1px;
  background: var(--rule-soft);
  border: 1px solid var(--rule-soft);
}
/* The competing options are read across, so they sit three to a row and stay
   comparable rather than stacking into a list. */
.mlab__grid--three { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }

.mlab h3 { margin: 2rem 0 0.5rem; }
.mlab h3:first-of-type { margin-top: 1.6rem; }
.mlab__lede { margin: 0 0 0.9rem; }

.mspec {
  margin: 0;
  background: var(--ground);
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* Every stage is the same height, so effects are compared and not just seen. */
.mspec__stage {
  position: relative;
  /*
     A FIXED floor, and deliberately not `flex: 1`.

     Flexing the stage bottom-aligns the captions, which sounds right and is
     not: caption text runs two lines in one specimen and four in the next, so
     bottom-alignment drives the TOPS apart and the hairline above each label
     goes ragged across the row. That rule is the structure the eye reads.

     Tall enough for the tallest resting specimen -- the annotation panel, at
     12rem -- so every stage in a row matches and every hairline lines up.
  */
  min-height: 12rem;
  display: grid;
  place-items: center;
  padding: 1.1rem;
  /* Room for a hover lift, rather than overflow -- a clipped specimen would
     misreport the effect it is specimen of. */
  overflow: hidden;
}
.mspec__cap {
  border-top: 1px solid var(--rule-soft);
  padding: 0.7rem 0.9rem 0.85rem;
  display: grid;
  gap: 0.3rem;
}
.mspec__n {
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--lum-max);
}
.mspec__t { font-size: 0.62rem; color: var(--amber-mid); word-spacing: 0.1em; }
.mspec__w { font-size: 0.68rem; line-height: 1.55; color: var(--dim); }
.mspec__cap .mlab__btn { justify-self: start; margin-top: 0.35rem; padding: 0.25rem 0.55rem; font-size: 0.6rem; }

/* A / B / C markers on the competing options. Ordinal, because these are a
   choice between three and not a sequence of three. */
.mspec__pick {
  font-size: var(--t-micro);
  color: var(--ground);
  background: var(--amber-mid);
  padding: 0 0.4rem;
  margin-right: 0.45rem;
}

/* --- specimen stages ----------------------------------------------------- */

.mspec .caveat, .mspec .stats, .mspec .ruler { width: 100%; }
.mspec .caveat { margin: 0; }
/* A 514x900 still at full stage width made this row three times the height of
   every other one, which defeats the point of showing them side by side. */
.mspec .tile { width: 5rem; }
.m-still { display: block; width: 100%; max-width: 7.5rem; }
.m-still img { display: block; width: 100%; height: auto; background: var(--ground-3); }

/* Isolation: the lamp specimen loses the sweep, the sweep specimen loses the
   lamp. Same component, one effect at a time. */
.mspec--lamp .caveat::after { display: none; }
.mspec--sweep .caveat__k::before { animation: none; opacity: 0.2; }

/* The caret, alone, at the size it runs at. The negative margin it carries in
   the typeline is kerning against typed text that is not here. */
.m-caretline { display: flex; align-items: baseline; gap: 0.35rem; color: var(--dim); font-size: var(--t-data); }
.m-caretline .typeline__caret { margin-left: 0; }

/*
   Hover specimens replay too. `.play` applies exactly what `:hover` applies,
   so the replay button is showing the real state and not an imitation of it.
*/
.mspec--link .m-link {
  color: var(--text); text-decoration: none; font-size: var(--t-data);
  transition: color var(--m-fast) linear, transform var(--m-fast) linear;
}
.mspec--link .m-link:hover, .mspec--link.play .m-link { color: var(--lum-max); }
.mspec--link .m-link:active { color: var(--amber); transform: translateY(calc(var(--m-near) / 3)); }
.mspec--link .m-link:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; color: var(--lum-max); }

.mspec--tile.play .tile { transform: scale(1.16) translateY(calc(var(--m-near) * -1)); }
.mspec--tile.play .tile::before { opacity: 1; }
.mspec--tile.play .tile img { transform: scale(1.06); }

.mspec--stat.play .stat::after { transform: scaleX(1); }
.mspec--stat.play .stat { background: var(--ground-3); }

/*
   ONE-SHOT SPECIMENS REST IN THE ARRIVED STATE.

   This is the site's oldest rule and it applies hardest here: never let a
   transition be what brings something into existence. If these rested at
   opacity 0 waiting for a replay, a reader who opened the design page in a
   background tab -- where no transition ticks and no observer fires -- would
   come back to a row of empty boxes and conclude the section was broken.

   So the resting state is "already arrived", and `.play` runs an animation
   FROM the hidden state back to it. Remove the class, remove the script,
   disable animation entirely: the specimen is still correct, it just stops
   being able to demonstrate itself. That is the right failure.
*/
@keyframes m-rise { from { opacity: 0; transform: translateY(var(--m-far)); } }
@keyframes m-fade { from { opacity: 0; } }
@keyframes m-draw { from { transform: scaleX(0); } }

.mspec--reveal.play .caveat { animation: m-rise var(--m-slow) var(--m-emph); }

/*
   The ruler, replayable. `--i` is already on every span, set by the builder.

   The transition is declared once, on .mlab .ruler .span further down, because
   the hover proposal needs to add to it rather than replace it -- a second
   `transition` on a more specific selector would silently drop the draw.
*/
/* `backwards` holds scaleX(0) through the stagger delay, which is the whole
   point of a stagger -- without it every bar is drawn before its turn. */
.mspec--ruler.play .span {
  animation: m-draw var(--m-slow) var(--m-enter) calc(var(--i, 0) * var(--m-stagger)) backwards;
}

/* The close control, with its own colour. */
.mspec--close .lb__close { position: static; }

/* --- signature 1: stepping between stills -------------------------------- */
/*
   Three answers to the most-used interaction on the site. The stage holds two
   stills; replay steps forward. Whichever is chosen ships in the lightbox.
*/
.m-step { position: relative; width: 100%; max-width: 7.5rem; }
.m-step img { display: block; width: 100%; height: auto; background: var(--ground-3); }
.m-step__b { position: absolute; inset: 0; }
/* contain, never cover. A specimen that crops to fit would be demonstrating a
   bug the lightbox was explicitly fixed to avoid. */
.m-step__b img { height: 100%; object-fit: contain; object-position: top left; }

/* A: hold. The readout changes, the image cuts. What ships today. */
.mspec--step-a .m-step__b { opacity: 0; }
.mspec--step-a.flip .m-step__b { opacity: 1; }

/* B: crossfade. */
.mspec--step-b .m-step__b { opacity: 0; transition: opacity var(--m-fast) linear; }
.mspec--step-b.flip .m-step__b { opacity: 1; }

/* C: directional settle. The outgoing frame leaves the way you sent it. */
.mspec--step-c .m-step__a,
.mspec--step-c .m-step__b {
  transition: opacity var(--m-base) linear, transform var(--m-base) var(--m-enter);
}
.mspec--step-c .m-step__b { opacity: 0; transform: translateX(var(--m-near)); }
.mspec--step-c.flip .m-step__b { opacity: 1; transform: none; }
.mspec--step-c.flip .m-step__a { opacity: 0; transform: translateX(calc(var(--m-near) * -1)); }

.m-step__f {
  position: absolute; left: 0; bottom: -1.35rem;
  font-size: 0.56rem; letter-spacing: 0.04em; color: var(--amber-mid);
}
.m-step { margin-bottom: 1.35rem; }

/* --- signature 2: how a value arrives ------------------------------------ */

/* Per-cell index, so the stagger works without the script touching these. */
.mspec .stat:nth-child(1) { --i: 0; }
.mspec .stat:nth-child(2) { --i: 1; }
.mspec .stat:nth-child(3) { --i: 2; }

/*
   All three rest in the arrived state: figures readable, channel ticks drawn.
   Replay runs the arrival. B and C leave the tick drawn afterwards because in
   both of them the tick IS the arrival -- it is not a hover affordance here.
*/
.mspec--arr-b .stat::after, .mspec--arr-c .stat::after { transform: scaleX(1); }

/* A: rise. The whole block travels 20px. What ships today, retimed to --m-slow. */
.mspec--arr-a.play .stats { animation: m-rise var(--m-slow) var(--m-emph); }

/* B: settle. No travel at all. The figures fade up and the ticks draw across. */
.mspec--arr-b.play .stats { animation: m-fade var(--m-base) linear; }
.mspec--arr-b.play .stat::after {
  animation: m-draw var(--m-slow) var(--m-enter) calc(var(--i) * var(--m-stagger)) backwards;
}

/* C: latch. The value is simply there, then the readout confirms it. */
.mspec--arr-c.play .stats { animation: m-fade var(--m-fast) linear; }
.mspec--arr-c.play .stat::after {
  animation: m-draw var(--m-base) var(--m-enter)
             calc(var(--m-fast) + var(--i) * var(--m-stagger)) backwards;
}

/* --- proposals ----------------------------------------------------------- */
/*
   Scoped to the lab. Neither ships until you say so; both are one line to
   promote, by dropping the `.mlab` prefix.
*/

/* The legend answers which fill you are asking about. */
.mlab .legend:hover .legend__item { transition: opacity var(--m-fast) linear; }
.mlab .legend:hover .legend__item:not(:hover) { opacity: 0.4; }

/*
   A span says it is worth pointing at. Every one of them carries a `title`
   with the reason for its fill, and nothing suggested they were worth asking.

   This is also where the ruler's draw transition is declared, so the two
   compose instead of the later one dropping the earlier.
*/
.mlab .ruler .span {
  transition: transform var(--m-slow) var(--m-enter) calc(var(--i, 0) * var(--m-stagger)),
              box-shadow var(--m-fast) linear;
}
.mlab .ruler .span:hover { box-shadow: 0 0 0 1px var(--amber); }

/* --- the shop ------------------------------------------------------------ */
/*
   Uses the same tokens as everything else -- the palette, the type scale, the
   motion durations -- so the shop reads as part of the site rather than as a
   storefront bolted to it. Nothing here is new design language; it is the
   existing one arranged into cards.
*/

/*
   RESPONSIVE WITHOUT MEDIA QUERIES.

   `repeat(auto-fit, minmax(15rem, 1fr))` says: fit as many columns as will
   hold 15rem each, then share the leftover space between them equally. At a
   phone width that resolves to one column, on a laptop to three or four, and
   it re-flows continuously in between. No breakpoints to maintain.
*/
.pgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1px;
  background: var(--rule-soft);     /* the gaps ARE the hairlines between cards */
  border: 1px solid var(--rule-soft);
  margin-top: 1.8rem;
}
.pgrid--small { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }

/* The whole card is one link, so the target is the card and not just a word. */
.pcard {
  display: flex;
  flex-direction: column;
  background: var(--ground);
  text-decoration: none;
  color: inherit;
  transition: background var(--m-fast) linear;
}
.pcard:hover { background: var(--ground-2); }
.pcard:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
.pcard:active { background: var(--ground-3); }

/*
   `aspect-ratio` reserves the image's box before the file loads, so the page
   does not jump as images arrive. The img also carries width/height
   attributes, which do the same job for browsers that get there first -- both
   are cheap and they back each other up.
*/
.pcard__img {
  display: block;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--ground-2);
  border-bottom: 1px solid var(--rule-soft);
}
.pcard__img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--m-base) var(--m-enter);
}
.pcard:hover .pcard__img img { transform: scale(1.03); }

.pcard__body { display: flex; flex-direction: column; gap: 0.45rem; padding: 0.95rem 1rem 1.1rem; }
.pcard__cat {
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--amber-mid);
}
.pcard__n { font-size: 0.92rem; color: var(--lum-max); letter-spacing: -0.01em; }
.pcard__d { font-size: 0.76rem; line-height: 1.6; color: var(--dim); }

/* `margin-top: auto` pushes this row to the bottom of the card, so price and
   status line up across a row whatever length the descriptions are. */
.pcard__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  margin-top: auto;
  padding-top: 0.55rem;
}
.pcard__p { font-size: 0.86rem; color: var(--lum-max); font-variant-numeric: tabular-nums; }
.pcard__s {
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--amber);
}
.pcard__s--off { color: var(--dim); }

/* --- the buy control ------------------------------------------------------ */

.buy {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 2.75rem;              /* 44px: the touch-target floor */
  padding: 0 1.3rem;
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--amber);
  border: 1px solid var(--amber-low);
  background: var(--amber-wash);
  transition: background var(--m-fast) linear, border-color var(--m-fast) linear,
              color var(--m-fast) linear, transform var(--m-fast) linear;
}
.buy__x { transition: transform var(--m-base) var(--m-enter); }
.buy:hover { border-color: var(--amber); color: var(--lum-max); background: rgba(232, 163, 61, 0.16); }
.buy:hover .buy__x { transform: translateX(var(--m-near)); }
.buy:focus-visible { outline: 2px solid var(--amber); outline-offset: 3px; color: var(--lum-max); }
.buy:active { background: var(--amber-low); color: var(--ground); transform: translateY(calc(var(--m-near) / 3)); }

/*
   The not-for-sale state is a <span>, not a disabled <button>. There is no
   form here and nothing to submit, so a real button would be a control that
   promises an action and does nothing. A flat label is the honest version, and
   it needs no hover, focus or press state because it is not interactive.
*/
.buy--off {
  color: var(--dim);
  border-color: var(--rule);
  border-style: dashed;
  background: none;
  cursor: default;
}

/* --- a product page ------------------------------------------------------- */

.pdetail { display: grid; gap: clamp(1.4rem, 4vw, 3rem); }
@media (min-width: 52rem) {
  /* Picture left, everything else right, once there is room for two columns. */
  .pdetail { grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); align-items: start; }
}
.pdetail__img { border: 1px solid var(--rule-soft); background: var(--ground-2); }
/* height:auto alongside width:100% keeps the aspect ratio intact -- without it
   the width and height attributes on the tag fight the CSS and the image
   distorts. This has bitten this site before; see design-system.md. */
.pdetail__img img { display: block; width: 100%; height: auto; }
/* The image opens in place, as a gallery still does (product_panel in build.py). */
a.pdetail__img { display: block; cursor: zoom-in; }
/* a one-image panel's empty arrow slots: they hold the grid, they show nothing */
.lb__step--none { visibility: hidden; pointer-events: none; }
a.pdetail__img:hover, a.pdetail__img:focus-visible { border-color: var(--rule); }
/* A product's other views, beneath the large one: front, back, detail. */
.pdetail__views { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 0.7rem; }
.pdetail__views a {
  display: block; width: 4.6rem; cursor: zoom-in; text-decoration: none;
  font-family: var(--mono); font-size: var(--t-micro); color: var(--dim);
  letter-spacing: var(--track-label); text-transform: uppercase;
}
.pdetail__views img {
  display: block; width: 100%; height: auto; margin-bottom: 0.3rem;
  border: 1px solid var(--rule-soft); background: var(--ground-2);
}
.pdetail__views a:hover img, .pdetail__views a:focus-visible img { border-color: var(--rule); }
.pdetail__meta { min-width: 0; }
.pdetail__price {
  font-size: 1.3rem;
  color: var(--lum-max);
  font-variant-numeric: tabular-nums;
  margin: 0 0 1.2rem;
}
.pdetail__buy { margin: 1.6rem 0 0.8rem; }
.pdetail .fields { margin-top: 1.6rem; }

/* --- responsive ---------------------------------------------------------- */

/* --- mobile navigation --------------------------------------------------- */

/*
   60rem = 960px, not 768px.

   The header's content measures about 768px wide -- brand, six mono links,
   gaps and gutters -- so a 768px breakpoint meant the nav collapsed at the
   exact width it started wrapping, and was visibly cramped for the 200px
   above that. It also meant a window had to be dragged very narrow before
   anything happened, which reads as "the mobile view is broken" rather than
   "the breakpoint is low". Collapse early; the header is calmer for it.
*/
@media (max-width: 59.99rem) {
  .masthead { padding-right: calc(var(--gut) + 2.6rem); }

  .nav-burger {
    display: grid;
    place-items: center;
    position: absolute;
    top: 0.55rem;
    right: calc(var(--gut) - 0.4rem);
    width: 2.6rem;
    height: 2.6rem;
    cursor: pointer;
  }
  /* Three rules that become an X. */
  .nav-burger span,
  .nav-burger span::before,
  .nav-burger span::after {
    content: "";
    display: block;
    width: 1.15rem;
    height: 1px;
    background: var(--lum-max);
    transition: transform var(--m-base) var(--m-enter), opacity var(--m-fast) linear;
  }
  .nav-burger span::before { transform: translateY(-0.4rem); }
  .nav-burger span::after { transform: translateY(0.4rem); }

  .nav-check:checked ~ .nav-burger span { background: transparent; }
  .nav-check:checked ~ .nav-burger span::before { transform: rotate(45deg); background: var(--amber); }
  .nav-check:checked ~ .nav-burger span::after { transform: rotate(-45deg); background: var(--amber); }
  .nav-check:focus-visible ~ .nav-burger { outline: 2px solid var(--amber); outline-offset: 2px; }

  .nav {
    display: none;
    width: 100%;
    margin-left: 0;
    flex-direction: column;
    gap: 0;
    padding-top: 0.6rem;
  }
  .nav-check:checked ~ .nav { display: flex; }
  .nav a {
    padding: 0.7rem 0;
    border-top: 1px solid var(--rule-soft);
    font-size: 0.86rem;
  }
}

@media (max-width: 42rem) {
  .ruler__row { grid-template-columns: 1fr auto; row-gap: 0.25rem; }
  .ruler__track { grid-column: 1 / -1; }
  .fields { grid-template-columns: 1fr; gap: 0; }
  .fields .k { padding-bottom: 0; border-top: 1px solid var(--rule-soft); }
  .fields .v { padding-top: 0.15rem; border-top: 0; padding-bottom: 0.7rem; }
  .nav { width: 100%; margin-left: 0; }
  .jump a { grid-template-columns: 1fr; gap: 0.25rem; }
  .jump .jump__d { text-align: left; }
  .display { letter-spacing: -0.03em; }
}

/* --- reduced motion ------------------------------------------------------ */
/*
   The fallback is an INSTANT STATE CHANGE, not an absence.

   A tile still lifts, a panel still opens, a block still arrives -- each one
   simply gets there in a single frame. The end state is what carries the
   meaning and it is never withheld; only the travel and the duration go. That
   is the difference between honouring the preference and punishing it.

   Done by redefining the tokens rather than by overriding declarations. There
   is only one motion vocabulary, so there is only one place to turn it down,
   and any effect added later inherits the fallback without being told to.
*/
@media (prefers-reduced-motion: reduce) {
  /* `.mlab` is listed for the same reason it is listed where the tokens are
     declared: it re-declares the arithmetic, so reducing only `:root` would
     leave the design page's specimens running at full speed for exactly the
     reader who asked them not to. */
  :root, .mlab {
    --m-fast: 1ms;
    --m-base: 1ms;
    --m-slow: 1ms;
    --m-near: 0px;
    --m-far: 0px;
    --m-stagger: 0ms;
  }

  /*
     The ambient loops STOP rather than run fast.

     Setting an infinite animation to a millisecond does not calm it down, it
     turns it into a strobe -- which is the precise thing this preference
     exists to prevent. The caret parks solid, which is also the more honest
     readout: the cursor is still there, it has just stopped blinking.
  */
  h1.page-title::after, .ty-caret, .typeline__caret, .caveat__k::before {
    animation: none;
    opacity: 1;
  }
  /* The sweep has nowhere good to park, so it leaves rather than freezing
     as a bright bar across the top of every panel. */
  .annot::after, .caveat::after, .player__note::after { animation: none; opacity: 0; }

  /*
     Backstop, kept from the previous version. The tokens cover everything in
     this file; this covers anything that ever escapes them.
  */
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
}

@media print {
  body::before { display: none; }
  body { background: #fff; color: #000; }
  .masthead, .nav, .updown { display: none; }
}

/* --- the book ---------------------------------------------------------------
   The coloring book's page and the home page's pointer to it. The images are
   exported already toned to this site's range -- ink at --ground, paper at
   --lum-max -- so nothing here is brighter than the render.

   The buy link (.bookbuy -- not .buy, which is the shop's control on
   feat/shop) is amber, the site's one action colour. Red is reserved for
   leaving a panel, and a purchase is not that.
*/
.book {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.25rem);
  align-items: start;
  margin-top: 1.4rem;
}
@media (max-width: 59.99rem) {
  .book { grid-template-columns: minmax(0, 1fr); }
  .book__cover { max-width: 20rem; }
}
.book__cover, .book__spread, .plates__f { margin: 0; }
.book__cover img, .book__spread img, .plates__f img, .booklink__cover img {
  display: block;
  width: 100%;
  height: auto;           /* both dimensions auto: see "max-width and max-height" */
  border: 1px solid var(--rule);
  background: var(--ground-2);
}
.book__spread { margin-bottom: 1rem; }
.book__about .note { margin: 1.2rem 0; }
.book__seller { margin-top: 0.7rem !important; }

.bookbuy {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: 0.4rem;
  padding: 0.75rem 1.1rem;
  font-family: var(--mono);
  font-size: var(--t-data);
  letter-spacing: 0.04em;
  color: var(--amber);
  text-decoration: none;
  border: 1px solid var(--amber-mid);
  background: var(--amber-wash);
  transition: background var(--m-fast) linear, border-color var(--m-fast) linear;
}
.bookbuy:hover { border-color: var(--amber); background: rgba(232, 163, 61, 0.16); }
.bookbuy:active { background: var(--amber-low); }

.plates {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));   /* 8 plates: two even rows */
  gap: 1rem;
  margin-top: 1.4rem;
}
/* After the base rule, or the four columns above win at every width. */
@media (max-width: 59.99rem) {
  .plates { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.plates__f figcaption {
  margin-top: 0.45rem;
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--dim);
}

.booklink {
  display: grid;
  grid-template-columns: minmax(0, 9rem) minmax(0, 1fr);
  gap: clamp(1.2rem, 3vw, 2.2rem);
  align-items: start;
}
.booklink h2 { margin-top: 0; }
.booklink .jump { margin: 1rem 0 1.2rem; max-width: var(--measure); }
@media (max-width: 30rem) {
  .booklink { grid-template-columns: minmax(0, 1fr); }
  .booklink__cover { max-width: 9rem; }
}

/* --- the facility's manual (draft/facility) -------------------------------
   The manual's tables are prose in cells, so they wrap; a record table's
   nowrap would push them past a phone's edge. Figures fill the column and
   keep their aspect: width/height attributes + height:auto. */
.manual td, .manual th, .mread__text td, .mread__text th { white-space: normal; min-width: 0; }
.manual td:first-child { white-space: nowrap; }
/* the reader (manual_reader, site and drive): its tables fit the column, never
   scroll sideways -- a long word breaks rather than widen it (measured: the
   revision history was 587 px in a 574 px column, 'The facil' clipped) */
.mread__text table { width: 100%; }
.mread__text td:first-child { white-space: normal; }   /* its parts carry .manual too: the reader's cells all wrap */
.mread__text :is(td, th) { overflow-wrap: break-word; padding-right: 0.8rem; }   /* words stay whole; only a word longer than its column breaks */
/* on a narrow screen the cells give back their gutters, so a wide table still fits */
@media (max-width: 47.99rem) {
  .mread__text .tablewrap { --cell-in: 0.7rem; }
  .mread__text :is(td, th) { padding-right: 0.6rem; }
  .mread__text table { font-size: 0.7rem; }
  /* a phone: a headed table stacks, each row a block of labelled lines
     (data-label, from label_cells()), so no column is squeezed to letters */
  .mread__text table:has(td[data-label]) thead { display: none; }
  .mread__text table:has(td[data-label]) :is(tbody, tr, td) { display: block; }
  .mread__text table:has(td[data-label]) tr { border-top: 1px solid var(--rule-soft); padding: 0.55rem 0; }
  .mread__text table:has(td[data-label]) tr:first-child { border-top: 0; }
  .mread__text table:has(td[data-label]) td { border-top: 0; padding: 0.12rem var(--cell-in); overflow-wrap: break-word; }
  .mread__text td[data-label]::before {
    content: attr(data-label); display: block; color: var(--dim);
    font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
  }
  .mread__text td[data-label=""]::before { content: none; }
}
.mfig { margin: 2.2rem 0; }
.mfig img { display: block; width: 100%; height: auto; max-width: 100%; background: var(--ground-2); }
.mfig--half img { max-width: 34rem; }
.mfig--third img { max-width: 24rem; }
.mfig figcaption { margin-top: 0.7rem; font-family: var(--mono); font-size: 0.72rem; line-height: 1.6; color: var(--dim); }

/* the facility's front end: calls to action. The primary is the book's amber
   .bookbuy; the alternative is a quiet text link beside it. The gallery CTA is
   a framed block so the page's flow has somewhere to arrive. */
.ctas { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.6rem; margin-top: 1.8rem; }
.ctas .bookbuy { margin: 0; }
.ctas__alt { font-family: var(--mono); font-size: 0.8rem; color: var(--dim); text-decoration: none;
  border-bottom: 1px solid var(--rule); padding-bottom: 0.15rem; }
.ctas__alt:hover, .ctas__alt:focus-visible { color: var(--lum-max); border-color: var(--amber); }
.gcta { border: 1px solid var(--rule); background: var(--ground-2); padding: clamp(1.4rem, 4vw, 2.6rem); }
.gcta__h { margin: 0.4rem 0 0.8rem; max-width: 34rem; }
.endmark { max-width: var(--measure); display: flex; align-items: center; gap: 1.25rem; margin: 3.5rem 0 1rem; }
.endmark::before, .endmark::after { content: ""; flex: 1; height: 1px; background: var(--rule); }
.endmark img { flex: none; display: block; width: 36px; height: 36px; }

/* === launch: the home page as a product launch (launch/terminal) ============
   The facility sold like a flagship product, in a terminal: mono type, the
   amber caret, a prompt for every heading -- and a modern finish over it:
   full-bleed photography, rounded glass panels, soft light that follows the
   pointer. Built by page_front() in facility.py; motion in site.js (launch).

   The rule that runs through every effect here, as through the rest of the
   site: the FINISHED state is the CSS. A hero, a tile, a word of the
   statement is visible with no script at all; motion only adds. */

:root {
  --radius-l: 22px;
  --radius-m: 14px;
  --glass: rgba(17, 17, 18, 0.62);
  --glass-edge: rgba(180, 178, 172, 0.10);
  --shade: linear-gradient(180deg, rgba(10,10,11,0.15) 0%, rgba(10,10,11,0.35) 45%, rgba(10,10,11,0.94) 100%);
}

/* --- hero ----------------------------------------------------------------- */
.hero { position: relative; min-height: min(100svh, 62rem); display: grid; align-items: end;
  padding: clamp(5rem, 14vh, 9rem) 0 clamp(3.5rem, 9vh, 6rem); overflow: hidden; isolation: isolate; }
.hero__media { position: absolute; inset: 0; z-index: -1; transform: translateY(var(--hero-shift, 0)); }
.hero__media::after { content: ""; position: absolute; inset: 0; background: var(--shade); }
/* the photograph settles from a slight push-in: a finished frame that moves, never a reveal */
.hero__img { width: 100%; height: 100%; object-fit: cover; display: block;
  animation: hero-settle 2.4s var(--m-enter) both; }
@keyframes hero-settle { from { transform: scale(1.07); } }
.hero__copy { position: relative; opacity: var(--hero-fade, 1); transform: translateY(var(--hero-lift, 0)); }
.hero--image .lede--hero { color: var(--lum-max); text-shadow: 0 2px 24px rgba(0,0,0,0.6); }
.hero--image .eyebrow, .hero--image .lede--sub { text-shadow: 0 1px 12px rgba(0,0,0,0.8); }
.hero__cue { position: absolute; left: 50%; bottom: 1.4rem; transform: translateX(-50%);
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--dim); text-decoration: none; }
.hero__cue::after { content: ""; display: inline-block; width: 0.55em; height: 1em; margin-left: 0.35em;
  vertical-align: -0.15em; background: var(--amber); animation: caret var(--m-pulse) steps(1) infinite; }

/* --- statement: lit word by word as it scrolls through -------------------- */
.launch-statement { padding-block: clamp(5rem, 18vh, 11rem); border-top: 0 !important; }
.scrub { font-family: var(--mono); font-weight: 600; letter-spacing: var(--track-h1);
  font-size: clamp(1.9rem, 5.6vw, 4.4rem); line-height: 1.08; color: var(--lum-max);
  max-width: 18ch; margin: 0 auto; text-align: center; }
/* no colour transition: a transition started in a background tab freezes at
   its start value (CLAUDE.md), which here would leave words dim for good */
/* dimmed only once the script has said it will light them (finished state = lit) */
.js-scrub .scrub .w { color: var(--rule); }
.js-scrub .scrub .w.lit { color: var(--lum-max); }
.js-scrub .scrub .w.lit:last-child { color: var(--amber); }

/* --- chapters --------------------------------------------------------------- */
.chapter__grid { display: grid; gap: clamp(1.4rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 60rem) {
  .chapter__grid { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
  .chapter--sticky .chapter__grid { align-items: start; min-height: 125vh; }
  .chapter--sticky .chapter__media { position: sticky; top: 14vh; }
  .chapter--sticky .chapter__copy { padding-top: 38vh; }
}
.chapter__media { margin: 0; border-radius: var(--radius-l); overflow: hidden; background: var(--ground-2);
  border: 1px solid var(--glass-edge); }
.chapter__media img { display: block; width: 100%; height: auto; }
.chapter__h, .launch-h { font-family: var(--mono); font-weight: 600; color: var(--lum-max);
  letter-spacing: var(--track-h1); font-size: clamp(1.6rem, 3.6vw, 2.8rem); line-height: 1.05;
  margin: 0 0 1rem; }
.chapter__h::before, .launch-h::before { content: "> "; color: var(--amber); }
.chapter__p, .launch-sub { color: var(--text); font-size: var(--t-lede); max-width: 34rem; line-height: 1.6; }
.launch-h { margin-bottom: 1.6rem; }

/* --- bento ------------------------------------------------------------------ */
.bento { display: grid; gap: 0.9rem; grid-template-columns: 1fr; }
@media (min-width: 40rem) { .bento { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 60rem) {
  .bento { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 15rem; }
  .bento__tile--wide { grid-column: span 2; }
  .bento__tile--tall { grid-row: span 2; }
}
.bento__tile { position: relative; margin: 0; border-radius: var(--radius-l); overflow: hidden;
  background: var(--ground-2); border: 1px solid var(--glass-edge); min-height: 15rem; isolation: isolate;
  transition: transform var(--m-base) var(--m-enter), border-color var(--m-base) var(--m-enter); }
.bento__tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--m-slow) var(--m-enter); }
.bento__tile figcaption { position: absolute; left: 0.8rem; right: 0.8rem; bottom: 0.8rem; z-index: 2;
  padding: 0.7rem 0.9rem; border-radius: var(--radius-m); background: var(--glass);
  -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid var(--glass-edge); font-family: var(--mono); font-size: var(--t-data);
  color: var(--text); line-height: 1.45; }
.bento__tile figcaption b { color: var(--lum-max); font-weight: 600; }
@media (hover: hover) {
  .bento__tile:hover { transform: translateY(calc(var(--m-near) * -2)); border-color: var(--rule); }
  .bento__tile:hover img { transform: scale(1.04); }
}

/* pointer light: site.js sets --mx/--my on [data-glow]; without it, no light */
[data-glow] { position: relative; }
[data-glow]::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  opacity: 0; transition: opacity var(--m-base) var(--m-enter);
  background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 50%), rgba(232,163,61,0.13), transparent 60%); }
@media (hover: hover) { [data-glow]:hover::after { opacity: 1; } }

/* --- lifestyle rail --------------------------------------------------------- */
.rail { overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none;
  padding: 0.8rem 0 1.2rem; margin-top: 1.4rem; overscroll-behavior-x: contain; }
.rail::-webkit-scrollbar { display: none; }
.rail__track { display: flex; gap: 0.9rem; width: max-content;
  padding-inline: max(var(--gut), calc((100vw - var(--max)) / 2 + var(--gut))); }
.rail__item { margin: 0; flex: 0 0 auto; width: min(85vw, 36rem); border-radius: var(--radius-l); overflow: hidden;
  scroll-snap-align: center; background: var(--ground-2); border: 1px solid var(--glass-edge);
  transition: transform var(--m-base) var(--m-enter); }
.rail__item img { display: block; width: 100%; height: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
@media (hover: hover) { .rail__item:hover { transform: translateY(calc(var(--m-near) * -2)); } }

/* --- places wall ------------------------------------------------------------ */
.places { columns: 2 12rem; column-gap: 0.9rem; }
@media (min-width: 60rem) { .places { columns: 4; } }
.places__item { margin: 0 0 0.9rem; break-inside: avoid; border-radius: var(--radius-m); overflow: hidden;
  border: 1px solid var(--glass-edge); background: var(--ground-2); }
.places__item img { display: block; width: 100%; height: auto; transition: transform var(--m-slow) var(--m-enter); }
@media (hover: hover) { .places__item:hover img { transform: scale(1.04); } }

/* --- tech specs ------------------------------------------------------------- */
.specs { display: grid; grid-template-columns: minmax(8rem, 14rem) 1fr; margin: 0;
  border-top: 1px solid var(--rule); font-family: var(--mono); }
.specs dt, .specs dd { margin: 0; padding: 1rem 0; border-bottom: 1px solid var(--rule-soft); }
.specs dt { color: var(--dim); font-size: var(--t-data); text-transform: uppercase; letter-spacing: var(--track-label); }
.specs dd { color: var(--lum-max); font-size: clamp(1rem, 1.6vw, 1.25rem); }

/* --- closing ---------------------------------------------------------------- */
.closing { position: relative; min-height: 80svh; display: grid; place-items: center; text-align: center;
  overflow: hidden; isolation: isolate; padding: 6rem 0; border-top: 1px solid var(--rule-soft); }
.closing__media { position: absolute; inset: 0; z-index: -1; }
.closing__media::after { content: ""; position: absolute; inset: 0;
  background: radial-gradient(ellipse at 50% 60%, rgba(10,10,11,0.35), rgba(10,10,11,0.92) 75%); }
.closing__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.closing__h { font-family: var(--mono); font-weight: 600; color: var(--lum-max); margin: 0;
  font-size: clamp(2rem, 6vw, 4.6rem); letter-spacing: var(--track-display); }
.closing__h::after { content: ""; display: inline-block; width: 0.5em; height: 0.9em; margin-left: 0.15em;
  vertical-align: -0.05em; background: var(--amber); animation: caret var(--m-pulse) steps(1) infinite; }
.closing__p { color: var(--text); font-size: var(--t-lede); margin: 1rem 0 0; }
.closing .ctas { justify-content: center; }
/* over a photograph the wash-thin button disappears: give it a ground of its own */
.closing--image .bookbuy, .hero--image .bookbuy { background: rgba(10,10,11,0.72);
  border-color: var(--amber); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }

/* image reveal variant: a settle from a slight zoom, on the reveal system's
   [data-reveal] (which owns opacity and the stagger) */
.js-reveal [data-reveal="zoom"]:not(.in) { transform: scale(1.04); }
[data-reveal="zoom"] { transition: opacity var(--m-slow) var(--m-enter), transform var(--m-slow) var(--m-enter); }

/* --- click: a press, and a ripple from the pointer (site.js adds the span) -- */
.bookbuy { position: relative; overflow: hidden; transition: transform var(--m-fast) var(--m-enter); }
.bookbuy:active { transform: scale(0.97); }
.ripple { position: absolute; border-radius: 50%; pointer-events: none; width: 12px; height: 12px;
  margin: -6px 0 0 -6px; background: rgba(232,163,61,0.35); animation: ripple 600ms var(--m-enter) forwards; }
@keyframes ripple { to { transform: scale(40); opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .hero__img { animation: none; }
  .bento__tile, .bento__tile img, .rail__item, .places__item img, [data-reveal="zoom"] { transition: none; }
  .ripple { display: none; }
}

/* --- launch, second pass: local nav, scrolling, actions, the marks ---------- */

/* Vertical scrolling: anchors glide, and land BELOW the two sticky bars rather
   than under them. --mast-h is measured by site.js; the fallback is close. */
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
:root { --mast-h: 3.1rem; --local-h: 0rem; }   /* no local nav since 2026-10-02 */
main [id] { scroll-margin-top: calc(var(--mast-h) + var(--local-h) + 0.6rem); }

/* the page's own bar: product name, sections, the action; sticks under the masthead */
.localnav { position: sticky; top: var(--mast-h); z-index: 30; height: var(--local-h);
  background: rgba(10, 10, 11, 0.78); -webkit-backdrop-filter: saturate(160%) blur(18px);
  backdrop-filter: saturate(160%) blur(18px); border-bottom: 1px solid var(--rule-soft); }
.localnav__in { height: 100%; display: flex; align-items: center; gap: 1.4rem; }
.localnav__id { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--lum-max);
  text-decoration: none; font-weight: 600; white-space: nowrap; }
.localnav__id .cube { width: 20px; height: 20px; flex: none; }
.localnav__links { display: flex; gap: 1.2rem; margin-left: auto; overflow-x: auto;
  overflow-y: hidden; scrollbar-width: none; font-size: var(--t-data); }
.localnav__links::-webkit-scrollbar { display: none; }
.localnav__links a { color: var(--dim); text-decoration: none; white-space: nowrap; }
.localnav__links a:hover, .localnav__links a.is-here { color: var(--lum-max); }
.localnav__cta { flex: none; padding: 0.35rem 0.9rem; border-radius: 980px; text-decoration: none;
  font-size: var(--t-data); background: var(--amber); color: var(--ground); white-space: nowrap; }
.localnav__cta:hover { filter: brightness(1.1); }
/* scroll progress: a hairline the width of the page read so far (site.js) */
/* reading progress: a hairline along the foot of the masthead, on every page
   (templates/base.html); site.js sets --read from 0 to 1 */
.progress { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; pointer-events: none; }
.progress span { display: block; height: 100%; background: var(--amber);
  transform-origin: 0 50%; transform: scaleX(var(--read, 0)); }
@media (max-width: 47.99rem) {
  .localnav__links { display: none; }       /* the CTA and the name are enough on a phone */
  .localnav__in { justify-content: space-between; }
}

/* the marks: the full emblem in the hero and at the close, the small cube as a section marker */
.hero__emblem { display: block; width: clamp(72px, 9vw, 112px); height: auto; margin-bottom: 1.2rem;
  filter: drop-shadow(0 6px 30px rgba(0, 0, 0, 0.6)); }
.closing__emblem { display: block; width: clamp(140px, 20vw, 240px); height: auto; margin: 0 auto 1.6rem; }
.eyebrow .cube { width: 1.15em; height: 1.15em; vertical-align: -0.2em; margin-right: 0.45em; }

/* the action at the end of a section */
.section-cta { margin-top: clamp(1.6rem, 3vw, 2.4rem); }
.chapter__copy .section-cta { margin-top: 1.4rem; }

/* horizontal scrolling: arrows either side, drag with a mouse, edges that fade */
.rail-wrap { position: relative; }
.rail { scroll-behavior: smooth; cursor: grab;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 4%, #000 96%, transparent 100%); }
.rail.is-dragging { cursor: grabbing; scroll-behavior: auto; scroll-snap-type: none; }
.rail.is-dragging img { pointer-events: none; }
.rail__btn { display: none; position: absolute; top: 50%; z-index: 3; transform: translateY(-50%);
  width: 2.8rem; height: 2.8rem; border-radius: 50%; border: 1px solid var(--glass-edge);
  background: var(--glass); color: var(--lum-max); font-size: 1.6rem; line-height: 1; cursor: pointer;
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  transition: opacity var(--m-base) var(--m-enter), transform var(--m-fast) var(--m-enter); }
.rail__btn--prev { left: max(0.6rem, calc((100vw - var(--max)) / 2)); }
.rail__btn--next { right: max(0.6rem, calc((100vw - var(--max)) / 2)); }
.rail__btn:active { transform: translateY(-50%) scale(0.94); }
.rail__btn[disabled] { opacity: 0.25; cursor: default; }
/* buttons need the script; without it the rail still scrolls by touch, wheel and keys */
.js-rail .rail__btn { display: grid; place-items: center; }
@media (hover: none) { .js-rail .rail__btn { display: none; } }

/* --- the hero as a rotating banner (facility.py BANNER, site.js banner) ------
   Slides are stacked; the one with .is-on is opaque. That is the FINISHED state
   and it is in the HTML: without scripting the first slide simply stays. The
   script only moves .is-on, and only while the tab is visible, so a background
   tab cannot strand a half-faded slide (CLAUDE.md, the hidden tab). */
.hero__media .slide { position: absolute; inset: 0; margin: 0; opacity: 0;
  transition: opacity 1.2s var(--m-enter); }
.hero__media .slide.is-on { opacity: 1; }
.hero__media .slide img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* a slow push-in on the slide that is showing, restarted each time it comes round */
.hero__media .slide.is-on img { animation: banner-push 7s linear both; }
@keyframes banner-push { from { transform: scale(1.06); } to { transform: scale(1); } }
.slide__cap { position: absolute; left: max(var(--gut), calc((100vw - var(--max)) / 2 + var(--gut)));
  bottom: 1.4rem; z-index: 2; padding: 0.35rem 0.8rem; border-radius: 980px; font-size: var(--t-data);
  background: var(--glass); color: var(--lum-max); border: 1px solid var(--glass-edge);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.slide:not(.is-on) .slide__cap { visibility: hidden; }
.banner__ctl { position: absolute; right: max(var(--gut), calc((100vw - var(--max)) / 2 + var(--gut)));
  bottom: 1.5rem; z-index: 3; display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.7rem; border-radius: 980px; background: var(--glass);
  border: 1px solid var(--glass-edge); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.banner__ctl[hidden] { display: none; }
.banner__dot { width: 0.5rem; height: 0.5rem; padding: 0; border: 0; border-radius: 980px; cursor: pointer;
  background: var(--dim); transition: width var(--m-base) var(--m-enter), background var(--m-base) linear; }
.banner__dot.is-on { width: 1.4rem; background: var(--lum-max); }
.banner__pause { width: 1.2rem; height: 1.2rem; padding: 0; border: 0; background: transparent;
  cursor: pointer; display: grid; place-items: center; margin-left: 0.2rem; }
.banner__pause span { width: 0.55rem; height: 0.65rem; border-left: 2px solid var(--lum-max);
  border-right: 2px solid var(--lum-max); }
.banner__pause[aria-pressed="true"] span { border: 0; width: 0; height: 0;
  border-left: 0.6rem solid var(--lum-max); border-top: 0.36rem solid transparent;
  border-bottom: 0.36rem solid transparent; }
.hero__cue { display: none; }           /* the controls sit where the cue was */
@media (max-width: 47.99rem) { .slide__cap { bottom: 4.2rem; } }
@media (prefers-reduced-motion: reduce) {
  .hero__media .slide { transition: none; }
  .hero__media .slide.is-on img { animation: none; }
}

/* --- what it powers: a carousel of use-case cards (facility.py USES) -------- */
.rail--cards .rail__item { width: min(75vw, 22rem); display: flex; flex-direction: column; }
/* a lightbox's downloads: the secondary pill, one per channel shown */
.lb__dl { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1rem 0 0.2rem; }
/* --- the gallery's rail of the edit: portrait stills, each opening in place --- */
.rail--stills .rail__item { width: min(44vw, 15rem); display: flex; flex-direction: column;
  color: inherit; text-decoration: none; }
.rail--stills .rail__item img { aspect-ratio: auto; height: auto; }
.rail__cap { display: flex; justify-content: space-between; gap: 0.5rem; color: var(--dim); }
.usecard img { aspect-ratio: 3 / 4; height: auto; }
.usecard figcaption { padding: 1.1rem 1.2rem 1.4rem; display: grid; gap: 0.45rem; }
.usecard__k { font-size: var(--t-micro); color: var(--dim); letter-spacing: var(--track-label);
  text-transform: uppercase; }
.usecard__h { color: var(--lum-max); font-size: 1.35rem; line-height: 1.15; letter-spacing: -0.02em; }
.usecard__q { color: var(--text); font-size: var(--t-data); line-height: 1.5; }

/* --- launch, third pass: rails that balance, brand furniture ---------------- */

/* Rails. Items snap to the CONTENT edge (where the headline above starts), not
   the centre: centring made the first card load half a gutter off, which read
   as the page being off-balance. The arrows live in a footer row below the
   rail, beside the section's action, never over the pictures.

   A scroll container clips everything at its edge -- including a hover glow
   (CLAUDE.md). So the rail carries padding the size of the largest glow and
   gives it back with negative margin: the layout does not move, the light
   has room to fall. */
.rail { scroll-snap-type: x mandatory;
  scroll-padding-inline: max(var(--gut), calc((100vw - var(--max)) / 2 + var(--gut)));
  padding-block: 2rem 4.5rem; margin-block: -1.2rem -3.4rem;
  -webkit-mask-image: none; mask-image: none; }
.rail .rail__item { scroll-snap-align: start; }
/* the far edge fades only while there is more to come (site.js sets data-at-end) */
.rail:not([data-at-end]) {
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 6rem), transparent 100%);
  mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 6rem), transparent 100%); }
.rail__track::after { content: ""; flex: 0 0 1px; }       /* the end padding, honoured by scroll */
.rail__foot { display: flex; align-items: center; justify-content: space-between; gap: 1rem 2rem;
  flex-wrap: wrap; margin-top: 0.4rem; }
.rail__foot .section-cta { margin: 0; }
.rail__ctl { display: flex; align-items: center; gap: 0.6rem; margin-left: auto; }
.rail__count { font-family: var(--code, var(--mono)); font-size: var(--t-micro); color: var(--dim);
  letter-spacing: 0.08em; min-width: 4.2em; text-align: right; font-variant-numeric: tabular-nums; }
.rail__btn { position: static; transform: none; display: none; }
.js-rail .rail__btn { display: grid; place-items: center; border-radius: 50%; }
.rail__btn:active { transform: scale(0.94); }
@media (hover: none) { .js-rail .rail__btn { display: grid; } }   /* phones get them too, below the rail */

/* the bento and the wall are not scroll containers, but give their glow the same courtesy */
.bento, .places { padding-bottom: 0.5rem; }

/* --- announcement bar: the line a company runs above its launch ------------- */
.announce { display: flex; align-items: center; justify-content: center; gap: 0.7rem;
  flex-wrap: wrap; padding: 0.65rem var(--gut); text-decoration: none; font-size: var(--t-data);
  color: var(--text); background: var(--ground-2); border-bottom: 1px solid var(--rule-soft);
  text-align: center; }
.announce__tag { font-family: var(--code, var(--mono)); font-size: var(--t-micro); text-transform: uppercase;
  letter-spacing: 0.12em; padding: 0.15rem 0.55rem; border-radius: 980px; color: #0a0a0b;
  background: var(--sunset, var(--amber)); }
.announce__go { color: var(--link, var(--amber)); white-space: nowrap; }
.announce__go span { display: inline-block; transition: transform var(--m-base) var(--m-enter); }
.announce:hover .announce__go span { transform: translateX(3px); }
.announce:hover { text-decoration: none; }

/* --- the status chip: a lamp and the facility's own figures ----------------- */
.status-chip { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 0.35rem 0.9rem; margin: 1.6rem 0 0; padding: 0.45rem 0.95rem; border-radius: 980px;
  font-family: var(--code, var(--mono)); font-size: var(--t-micro); text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--text); background: var(--glass); border: 1px solid var(--glass-edge);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.status-chip span + span::before { content: "/"; margin-right: 0.9rem; color: var(--rule); }
.status-chip .lamp + span::before { content: none; }
.lamp { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: #3ddc84;
  box-shadow: 0 0 0 0 rgba(61, 220, 132, 0.6); animation: lamp var(--m-breath, 2.6s) ease-out infinite; }
@keyframes lamp { 70% { box-shadow: 0 0 0 7px rgba(61, 220, 132, 0); } 100% { box-shadow: 0 0 0 0 rgba(61, 220, 132, 0); } }

/* --- the sun divider: the emblem's stripes, thin to thick -------------------- */
.sundivider { display: flex; justify-content: center; padding-block: clamp(1.5rem, 4vw, 3rem); }
.sundivider span { display: block; width: min(16rem, 50vw); height: 26px;
  background: var(--sunset, var(--amber));
  -webkit-mask-image: linear-gradient(#000 0 2px, transparent 2px 6px, #000 6px 9px, transparent 9px 13px,
    #000 13px 17px, transparent 17px 20px, #000 20px 26px);
  mask-image: linear-gradient(#000 0 2px, transparent 2px 6px, #000 6px 9px, transparent 9px 13px,
    #000 13px 17px, transparent 17px 20px, #000 20px 26px);
  border-radius: 0 0 999px 999px / 0 0 26px 26px; opacity: 0.9; }

/* --- labels: the mono, everywhere a value or a label is ---------------------- */
.usecard__k, .bento__tile figcaption b, .rail__count, .status-chip { font-feature-settings: "zero" 1; }

/* --- page heroes: a top-level page's header over a photograph of its own ----
   facility.py PAGE_ART turns a page's first section into .pagehero; the supply
   page builds one directly. The copy is the page's own header, unchanged, and
   sits where a plain page's header does: the wrap fills the band (as a grid
   item with auto margins it shrank to its own text and centred, so the left
   edge moved with each lede), and the copy starts at the plain header's top. */
.pagehero { position: relative; isolation: isolate; overflow: hidden; }
.pagehero--image { min-height: clamp(24rem, 56vh, 38rem); display: grid; align-items: start;
  padding-block: clamp(3.5rem, 9vw, 6rem) clamp(2.4rem, 6vh, 3.6rem) !important; }
.pagehero--image > .wrap { width: 100%; }
/* a narrow header keeps its measure, on the same left edge as the wide one */
.pagehero--image > .wrap--narrow { margin-left: max(0px, calc((100% - var(--max)) / 2)); }
.pagehero__media { position: absolute; inset: 0; z-index: -1; }
.pagehero__media img { width: 100%; height: 100%; object-fit: cover; display: block;
  animation: hero-settle 2.4s var(--m-enter) both; }
.pagehero__media::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.5) 45%, rgba(0,0,0,0.94) 100%); }
.pagehero--image { background: #000; }   /* a dark band in either theme, like the home hero */
.pagehero--image .lede, .pagehero--image .eyebrow { text-shadow: 0 1px 14px rgba(0,0,0,0.7); }
.pagehero .status-chip { margin-top: 1.2rem; }
@media (prefers-reduced-motion: reduce) { .pagehero__media img { animation: none; } }

/* the supply page's category bar and terms */
.chips { position: sticky; top: var(--mast-h); z-index: 20; background: var(--ground);
  border-block: 1px solid var(--rule-soft); }
.chips__in { display: flex; gap: 0.5rem; overflow-x: auto; overflow-y: hidden; padding-block: 0.7rem;
  scrollbar-width: none; }
.chips__in::-webkit-scrollbar { display: none; }
.chips a { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.9rem;
  border-radius: 980px; border: 1px solid var(--rule); color: var(--lum-max); text-decoration: none;
  font-family: var(--code, var(--mono)); font-size: var(--t-micro); text-transform: uppercase;
  letter-spacing: 0.12em; white-space: nowrap; min-height: 2rem; }
.chips a span { color: var(--dim); }
.chips a:hover { border-color: var(--lum-max); }
.catalogue + .catalogue { padding-top: 0; }
.terms { font-family: var(--code, var(--mono)); font-size: var(--t-micro); color: var(--dim);
  letter-spacing: 0.04em; max-width: 52rem; line-height: 1.7; }

/* --- the record, explorable in place (facility.py record_explorer) ----------
   Native <details>, nested: a group opens to its members, a member to its
   facts, and only then a link to the full page. Opening animates where the
   browser can size to "auto" (interpolate-size); elsewhere it simply opens.
   No script in any of it. */
:root { interpolate-size: allow-keywords; }
.explore { display: grid; gap: 0.6rem; }
.explore details > summary { list-style: none; cursor: var(--cur-link, pointer);
  display: grid; grid-template-columns: minmax(0, auto) auto minmax(0, 1fr) 1.2rem;
  align-items: center; gap: 0.4rem 0.9rem; }
.explore details > summary::-webkit-details-marker { display: none; }
/* the chevron: a quarter turn as it opens */
.explore details > summary::after { content: ""; justify-self: end; width: 0.5rem; height: 0.5rem;
  border-right: 1.5px solid var(--dim); border-bottom: 1.5px solid var(--dim);
  transform: rotate(-45deg); transition: transform var(--m-base) var(--m-enter); }
.explore details[open] > summary::after { transform: rotate(45deg); border-color: var(--lum-max); }
.explore__t { color: var(--lum-max); }
.explore__s { display: inline-flex; gap: 0.35rem; flex-wrap: wrap; }
.status--alt { opacity: 0.75; }
.explore__d { color: var(--dim); font-size: var(--t-data); grid-column: 3; text-align: right;
  overflow-wrap: anywhere; }
.explore__group > summary { padding: 1rem 1.2rem; }
.explore__group > summary .explore__t { font-size: 1.05rem; font-weight: 600; }
.explore__group > .explore__body { padding: 0 1.2rem 1.1rem; display: grid; gap: 0.25rem; }
.explore__item { border-left: 1px solid var(--rule); margin-left: 0.2rem; }
.explore__item > summary { padding: 0.65rem 0.4rem 0.65rem 1rem; border-radius: var(--radius-s, 8px); }
.explore__item > summary:hover { background: var(--ground-3); }
.explore__item > summary .explore__t { font-family: var(--code, var(--mono)); font-size: var(--t-data); }
.explore__item > .explore__body { padding: 0.2rem 0 0.9rem 1rem; }
.explore__item .fields { margin: 0 0 0.6rem; }
.explore__go, .explore__all { display: inline-flex; gap: 0.35rem; align-items: center;
  color: var(--link, var(--amber)); text-decoration: none; font-size: var(--t-data); }
.explore__all { margin-top: 0.6rem; padding-left: 0.2rem; }
.explore__go span, .explore__all span { transition: transform var(--m-base) var(--m-enter); }
.explore__go:hover span, .explore__all:hover span { transform: translateX(3px); }
.explore__note { color: var(--text); max-width: 46rem; margin: 0.2rem 0 0.4rem; }
/* a phone stacks the summary: name and chevron, then the chips, then the
   evidence line -- so a name never breaks to make room for its status */
@media (max-width: 40rem) {
  .explore details > summary { grid-template-columns: minmax(0, 1fr) 1.2rem; }
  .explore__t { grid-column: 1; grid-row: 1; }
  .explore__s { grid-column: 1; grid-row: 2; }
  .explore__d { grid-column: 1; grid-row: 3; text-align: left; }
  .explore details > summary::after { grid-column: 2; grid-row: 1; }
}
.explore__t { overflow-wrap: anywhere; }
/* the open, eased -- where the browser can animate to an automatic height */
@media (prefers-reduced-motion: no-preference) {
  .explore details::details-content { block-size: 0; overflow: clip;
    transition: block-size 450ms cubic-bezier(0.22, 1, 0.36, 1), content-visibility 450ms allow-discrete; }
  .explore details[open]::details-content { block-size: auto; }
}
