/* ═══════════════════════════════════════════════════════════════════════════
   OBSITH — the design system.

   Everything here is derived from the mark itself: near-black with a red cast,
   signal red, and the ember orange of metal at working heat. Nothing is loaded
   from another origin — both faces are self-hosted, so the page paints with the
   right type on the first frame and a network failure cannot change how it looks.
   ═══════════════════════════════════════════════════════════════════════════ */

/* CINZEL — cut from the letterforms on Roman monumental inscriptions. It is
   literally the shape of ancient carved stone, and its heavy weights carry the
   robustness the mark needs. Variable, so 400 through 900 costs one file. */
@font-face {
  font-family: 'Obsith Display';
  src: url('/assets/font/cinzel-latin-var.woff2') format('woff2-variations');
  font-weight: 400 900;
  font-display: swap;   /* the cinematic must never wait on a font */
  font-style: normal;
}
/* SPECTRAL — a serif drawn for screens. Prose in a serif reads as considered;
   the previous grotesque read as an interface. */
@font-face {
  font-family: 'Obsith Serif';
  src: url('/assets/font/spectral-400.woff2') format('woff2');
  font-weight: 400; font-display: swap; font-style: normal;
}
@font-face {
  font-family: 'Obsith Serif';
  src: url('/assets/font/spectral-600.woff2') format('woff2');
  font-weight: 600; font-display: swap; font-style: normal;
}
@font-face {
  font-family: 'Obsith Sans';
  src: url('/assets/font/inter-latin-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
}

:root {
  /* ── the forge palette ───────────────────────────────────────────────── */
  --ink:          #070405;   /* the mountain at night */
  --ink-2:        #0d0709;
  --ink-3:        #150a0d;
  --char:         #1d1215;   /* cooled slag */
  --iron:         #3a2b2e;

  --blood:        #b81420;   /* the mark's red */
  --blood-hot:    #e6202f;
  --blood-dim:    #6d0f18;

  --ember:        #ff7a2a;   /* metal at working heat */
  --ember-hot:    #ffb347;
  --ember-white:  #ffe6c4;   /* the hottest part of the fire */

  --bone:         #f3ece8;
  --bone-dim:     #b9aca8;
  --ash:          #7d7170;
  --ash-dim:      #4e4544;

  /* ── type ────────────────────────────────────────────────────────────── */
  --disp:  'Obsith Display', 'Trajan Pro', 'Optima', Georgia, serif;
  --serif: 'Obsith Serif', Georgia, 'Times New Roman', serif;
  --sans:  'Obsith Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace;

  /* ── rhythm ──────────────────────────────────────────────────────────── */
  --gut: clamp(1.25rem, 4vw, 3rem);
  --max: 68rem;

  /* ── motion ──────────────────────────────────────────────────────────── */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-in-out: cubic-bezier(.65, 0, .35, 1);

  color-scheme: dark;
}

/* ── reset ─────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--ink);
  color: var(--bone);
  font-family: var(--serif);
  font-size: clamp(1rem, .55vw + .85rem, 1.125rem);
  line-height: 1.72;
  font-feature-settings: 'kern' 1, 'liga' 1, 'cv05' 1;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, canvas, svg, video { display: block; max-width: 100%; }
a { color: inherit; }

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

::selection { background: var(--blood); color: var(--bone); }

/* Skip link — the cinematic is a canvas, so a keyboard visitor needs a way past it. */
.skip {
  position: fixed; left: 0; top: 0; z-index: 999;
  transform: translateY(-120%);
  background: var(--blood); color: var(--bone);
  padding: .7rem 1.1rem; font-weight: 700; text-decoration: none;
  transition: transform .18s var(--ease-out);
}
.skip:focus { transform: translateY(0); }

/* ═══ TYPE SCALE ═══════════════════════════════════════════════════════ */

.display {
  font-family: var(--disp);
  font-weight: 700;
  text-transform: uppercase;
  /* Inscriptional capitals were CUT with space around them. Tight tracking is
     what makes a Roman face look like a compressed sans pretending. */
  letter-spacing: .085em;
  line-height: 1.12;
  margin: 0;
}
h1.display  { font-size: clamp(2rem, 6vw, 4.4rem); font-weight: 800; }
h2.display  { font-size: clamp(1.5rem, 3.6vw, 2.4rem); }
h3.display  { font-size: clamp(1.1rem, 2vw, 1.5rem); letter-spacing: .1em; }

.eyebrow {
  font-family: var(--mono);
  font-size: .72rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--ember);
  margin: 0 0 1rem;
}
.eyebrow::before { content: '— '; color: var(--blood-hot); }

.lede {
  font-size: clamp(1.1rem, 1.4vw + .8rem, 1.45rem);
  line-height: 1.5;
  color: var(--bone);
  max-width: 46ch;
}
p { max-width: 66ch; }
.muted { color: var(--bone-dim); }
.small { font-size: .875rem; line-height: 1.6; }

/* the hot-metal gradient used on the motto and on key numbers */
.hot {
  background: linear-gradient(96deg, var(--ember-white) 0%, var(--ember-hot) 28%, var(--ember) 52%, var(--blood-hot) 88%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}

/* ═══ CHROME: the nav ═══════════════════════════════════════════════════ */

.chrome {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: flex; align-items: center; gap: 1rem;
  /* ⛔ SAFE-AREA INSETS, BECAUSE EVERY SERVED PAGE DECLARES viewport-fit=cover.
     That meta is not decoration: it explicitly opts the layout into the area
     UNDER the notch, the status bar, the rounded corners and the home indicator.
     All seven served pages carry it and this stylesheet had ZERO uses of
     env(safe-area-inset-*), so on any iPhone since the X the masthead — which
     holds the brand and the ONLY navigation — was drawn under the status bar.
     max() rather than an addition, so anything with no inset (every desktop)
     keeps exactly the padding it had. */
  padding: max(clamp(.85rem, 2vw, 1.4rem), env(safe-area-inset-top, 0px))
           max(var(--gut), env(safe-area-inset-right, 0px))
           clamp(.85rem, 2vw, 1.4rem)
           max(var(--gut), env(safe-area-inset-left, 0px));
  pointer-events: none;
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.chrome > * { pointer-events: auto; }
.chrome.is-hidden { opacity: 0; transform: translateY(-100%); pointer-events: none; }

.brand {
  display: inline-flex; align-items: center; gap: .6rem;
  text-decoration: none; font-family: var(--disp);
  font-size: 1.05rem; letter-spacing: .22em; text-transform: uppercase;
  font-weight: 700; color: var(--bone);
}
.brand img { width: 30px; height: auto; filter: drop-shadow(0 0 10px rgba(184,20,32,.55)); }

nav.tabs { margin-left: auto; display: flex; align-items: center; gap: clamp(.4rem, 1.6vw, 1.6rem); }
nav.tabs a {
  position: relative;
  font-family: var(--sans);
  font-size: .78rem; letter-spacing: .16em; text-transform: uppercase;
  font-weight: 600; text-decoration: none; color: var(--bone-dim);
  padding: .5rem .25rem;
  transition: color .25s var(--ease-out);
  white-space: nowrap;
}
nav.tabs a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: .18rem; height: 1px;
  background: var(--ember);
  transition: right .32s var(--ease-out);
}
nav.tabs a:hover, nav.tabs a[aria-current='page'] { color: var(--bone); }
nav.tabs a:hover::after, nav.tabs a[aria-current='page']::after { right: 0; }
nav.tabs a.is-product { color: var(--ember); }
nav.tabs a.is-product::after { background: var(--ember-hot); }

/* the sound toggle — reachable from anywhere, including mid-film */
.sound {
  display: inline-grid; place-items: center;
  width: 40px; height: 40px; flex: 0 0 auto;
  margin-left: .35rem;
  background: rgba(12,6,8,.80);
  border: 1px solid rgba(243,236,232,.16);
  border-radius: 50%;
  color: var(--bone); cursor: pointer;
  transition: border-color .25s var(--ease-out), background .25s var(--ease-out), transform .2s var(--ease-out);
}
.sound:hover { border-color: var(--ember); transform: scale(1.06); }
.sound svg { width: 17px; height: 17px; }
.sound .bars { transform-origin: center; }
.sound[data-state='off'] .bars { opacity: .18; }
.sound[data-state='on']  .bars { opacity: 1; }
.sound[data-state='on'] .b1 { animation: eq .9s var(--ease-in-out) infinite alternate; }
.sound[data-state='on'] .b2 { animation: eq .7s var(--ease-in-out) infinite alternate .15s; }
.sound[data-state='on'] .b3 { animation: eq 1.1s var(--ease-in-out) infinite alternate .3s; }
@keyframes eq { from { transform: scaleY(.35); } to { transform: scaleY(1); } }

/* ═══ BUTTONS ═══════════════════════════════════════════════════════════ */

.btn {
  --btn-bg: transparent;
  position: relative; display: inline-flex; align-items: center; gap: .7rem;
  padding: .95rem 1.7rem;
  font-family: var(--sans); font-size: .78rem; font-weight: 700;
  letter-spacing: .2em; text-transform: uppercase; text-decoration: none;
  color: var(--bone); background: var(--btn-bg);
  border: 1px solid rgba(243,236,232,.24);
  cursor: pointer; overflow: hidden;
  transition: color .3s var(--ease-out), border-color .3s var(--ease-out);
}
/* ⛔ THE FILL IS THE ELEMENT'S OWN BACKGROUND, NOT A z-index:-1 PSEUDO.
   John: "these buttons disappear when you have the mouse on." They did, and it
   was a real stacking bug rather than a colour choice. The fill lived on a
   ::before at z-index:-1; a negative z-index escapes the button and paints
   BEHIND its nearest stacking context, so on any card with a background of its
   own — the film end card, which is an opaque plate — the gradient went behind
   the card and never appeared, while :hover still switched the label to the dark
   ink colour meant to sit ON it. Dark text, no fill: an invisible button.
   Painting the gradient as the button's own background removes the stacking
   question entirely — text always paints above its element's background. */
.btn {
  background-image: linear-gradient(100deg, var(--blood), var(--ember));
  background-repeat: no-repeat;
  background-size: 100% 100%;
  background-position: 0 101%;              /* parked just below the box */
  transition: background-position .42s var(--ease-out),
              color .3s var(--ease-out), border-color .3s var(--ease-out),
              box-shadow .3s var(--ease-out);
}
.btn:hover, .btn:focus-visible {
  background-position: 0 0; border-color: transparent; color: var(--ink);
}
/* John: "they must blink when the mouse is on." A breathing ember halo — the
   button is already filled, so the pulse is light around it rather than the
   label switching on and off, which would make it unreadable half the time. */
@media (prefers-reduced-motion: no-preference) {
  .btn:hover, .btn:focus-visible { animation: btnBlink 1.25s var(--ease-out) infinite; }
}
@keyframes btnBlink {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255,122,42,.55), 0 6px 26px rgba(255,60,20,.28); }
  50%      { box-shadow: 0 0 0 6px rgba(255,122,42,0),  0 6px 34px rgba(255,60,20,.52); }
}
.btn--hot { border-color: var(--ember); color: var(--ember); }
.btn--hot:hover, .btn--hot:focus-visible { color: var(--ink); }

/* ═══ THE STAGE: the WebGL cinematic ════════════════════════════════════ */

/* BLACK ON LOAD. John: "when the site loads no background must exist, only
   black." The stage starts black and the scene fades UP only once the four
   quarters have locked, so the first thing on screen is the mark assembling out
   of nothing with no landscape competing for the eye. */
#stage {
  position: fixed; inset: 0; z-index: 0;
  background: #000;
  opacity: 0;
  transition: opacity 2.6s cubic-bezier(.4, 0, .2, 1);
}
#stage.is-revealed { opacity: 1; }
#stage canvas { width: 100%; height: 100%; display: block; }

/* the still that stands in for the cinematic before it loads, and for anyone
   whose device cannot run it. It is never a blank screen. */
/* The still that stands in for the scene is BLACK too. A warm gradient behind
   an assembling mark is exactly the background John asked not to be there; it
   only takes colour at the same moment the storm is allowed in. */
#poster {
  position: fixed; inset: 0; z-index: 1;
  background: #000;
  transition: opacity 1.1s var(--ease-out), background 2.6s var(--ease-out);
}
#poster.is-revealed {
  background:
    radial-gradient(46% 34% at 50% 86%, rgba(255,122,42,.30) 0%, rgba(120,16,22,.12) 44%, transparent 74%),
    radial-gradient(120% 95% at 50% 100%, #0b0406 0%, #050203 54%, #010001 100%);
}
#poster.is-gone { opacity: 0; pointer-events: none; }

/* ═══ HERO OVERLAY: logo assembly + component picker ════════════════════ */

.hero {
  position: relative; z-index: 10;
  /* ⛔ 100vh FIRST, THEN 100svh. svh is the right unit — the viewport with the
     mobile address bar SHOWING, so the hero never grows taller than what is
     actually visible and the motto never hides behind browser chrome. But a
     browser that does not know the unit DROPS the whole declaration, and with
     only the svh line present that leaves the hero with NO min-height at all on
     Safari below 15.4 — it collapses to its content. Two declarations lets the
     cascade do the fallback for free. */
  min-height: 100vh;
  min-height: 100svh;
  display: grid; grid-template-rows: 1fr auto;
  padding: clamp(4.5rem, 10vh, 7rem) var(--gut) clamp(1.4rem, 4vh, 2.4rem);
  pointer-events: none;
}
.hero > * { pointer-events: auto; }

/* The mark: four quadrants that fly in and lock. Sized in one place. */
/* THE MARK. John asked for the quarters to be THICK, "like bricks that can
   create a wall". So the mark lives in a 3D perspective context and each
   quarter carries a real extruded side: it is a slab of stone being set into
   place, not a picture sliding in. */
.mark {
  --mark: clamp(15rem, 34vmin, 26rem);
  --brick: clamp(10px, 1.6vmin, 18px);   /* the depth of the stone */
  position: relative;
  width: var(--mark); height: calc(var(--mark) * 0.959); /* 537/560 */
  margin: 0 auto;
  align-self: center;
  perspective: 1500px;
  perspective-origin: 50% 44%;
  transform-style: preserve-3d;
  filter: drop-shadow(0 14px 34px rgba(0,0,0,.6));
}
.mark__q {
  position: absolute; width: 50%; height: 50%;
  background-size: 100% 100%; background-repeat: no-repeat;
  will-change: transform, opacity, filter;
  opacity: 0;
  cursor: pointer;
  border: 0; padding: 0; margin: 0; background-color: transparent;
  -webkit-tap-highlight-color: transparent;
  transform-style: preserve-3d;
}
/* ⛔ THE EXTRUSION IS BUILT FROM STACKED SHADOWS, NOT A PSEUDO-ELEMENT.
   The obvious approach — a ::before pushed back by translateZ — was tried and
   RENDERED OVER THE ARTWORK: inside a preserve-3d button the pseudo-element did
   not sort behind its own element's background-image, so all four quarters came
   out as blank slabs and the mark disappeared. Measured, not theorised.

   A ladder of offset box-shadows cannot do that, because a box-shadow is always
   painted outside the border box and never over the background. Each rung is
   one pixel further out and one step darker, which is exactly how a cut face
   recedes, and the direction is AWAY FROM THE MARK'S CENTRE so the four read as
   one wall of stone rather than four tiles lit from four directions. */
.mark__q {
  --ex: 1px;            /* one rung of the extrusion ladder */
  transition: transform .45s var(--ease-out), box-shadow .45s var(--ease-out), filter .5s var(--ease-out);
}
.mark__q[data-q='1'] { --dx: -1; --dy: -1; }
.mark__q[data-q='2'] { --dx:  1; --dy: -1; }
.mark__q[data-q='3'] { --dx: -1; --dy:  1; }
.mark__q[data-q='4'] { --dx:  1; --dy:  1; }

/* ⛔ NO LADDER AT REST. Four quarters each casting a hard twelve-rung shadow
   outward add up to a dark frame around the whole mark — which reads as an
   outline on a picture, not as thickness in stone. The depth is kept for the
   moment it actually says something: the brick being drawn out of the wall. */
.mark.is-armed .mark__q:hover,
.mark.is-armed .mark__q:focus-visible {
  box-shadow:
    calc(var(--dx) * 1px)  calc(var(--dy) * 1px)  0 rgba(58,34,38,.98),
    calc(var(--dx) * 3px)  calc(var(--dy) * 3px)  0 rgba(46,26,30,.96),
    calc(var(--dx) * 5px)  calc(var(--dy) * 5px)  0 rgba(34,19,22,.94),
    calc(var(--dx) * 7px)  calc(var(--dy) * 7px)  0 rgba(23,13,15,.92),
    calc(var(--dx) * 9px)  calc(var(--dy) * 9px)  0 rgba(14,8,9,.90),
    calc(var(--dx) * 11px) calc(var(--dy) * 11px) 0 rgba(7,4,5,.90),
    calc(var(--dx) * 13px) calc(var(--dy) * 13px) 0 rgba(4,2,3,.90),
    calc(var(--dx) * 20px) calc(var(--dy) * 20px) 28px rgba(0,0,0,.7);
  /* ⛔ NO BLURRED SHADOW HERE. A blur spreads in every direction from its
     offset, so a soft shadow cast outward by one quarter reaches back across
     the quarter beside it — four of them drew a dark cross through the centre
     of the mark and mangled the wordmark. The slab's soft shadow is a single
     shadow on .mark, where it belongs. */
}
.mark__q[data-q='1'] { left: 0;   top: 0;   background-image: url('/assets/img/logo-q1.webp'); }
.mark__q[data-q='2'] { left: 50%; top: 0;   background-image: url('/assets/img/logo-q2.webp'); }
.mark__q[data-q='3'] { left: 0;   top: 50%; background-image: url('/assets/img/logo-q3.webp'); }
.mark__q[data-q='4'] { left: 50%; top: 50%; background-image: url('/assets/img/logo-q4.webp'); }

/* the pulse that travels quadrant → quadrant, one at a time, 2.5 s each */
/* ⛔ ONE drop-shadow, NOT TWO. Each drop-shadow is its own Gaussian blur pass
   over the element's alpha, and these run on four tiles that are transitioning
   for half a second on a ten-second loop that never stops. Two of them is two
   full blurs per quarter per frame for the glow alone. The same lesson already
   cost this session once, on the mark's seating shadow, where a stacked pair
   made a screenshot time out at sixty seconds. brightness() and saturate() stay
   — they are per-pixel arithmetic with no neighbourhood, i.e. nearly free; it is
   only the blurs that are expensive. */
.mark__q.is-lit {
  filter: brightness(1.55) saturate(1.3)
          drop-shadow(0 0 30px rgba(255,122,42,.82));
}
/* ⛔ TWO BLINKS, THEN HAND OVER TO THE NEXT QUARTER — the invitation to click.
   John: "Every quarter must blink twice clockwise so the user can understand
   he must click on." The lit state above is a STEADY highlight, and a steady
   highlight reads as decoration; a double blink reads as a prompt, which is
   the entire point of the sweep. Two iterations at 430 ms — the same 430 the
   sweep scheduler uses, so the visual and the schedule cannot drift apart.
   Only brightness/saturate move; the drop-shadow is held CONSTANT on purpose.
   Animating a blur means recomputing a full neighbourhood blur every frame
   for every quarter, which is what made a screenshot time out at sixty
   seconds earlier in this session. */
.mark__q.is-blinking { animation: quadBlink .43s var(--ease-out) 2; }
@keyframes quadBlink {
  0%   { filter: brightness(1.00) saturate(1.00) drop-shadow(0 0 30px rgba(255,122,42,.35)); }
  45%  { filter: brightness(2.15) saturate(1.45) drop-shadow(0 0 30px rgba(255,150,60,.95)); }
  100% { filter: brightness(1.00) saturate(1.00) drop-shadow(0 0 30px rgba(255,122,42,.35)); }
}
@media (prefers-reduced-motion: reduce) {
  /* The prompt must still be legible without motion, so it becomes a steady
     highlight rather than vanishing. */
  .mark__q.is-blinking { animation: none;
    filter: brightness(1.6) saturate(1.3) drop-shadow(0 0 30px rgba(255,122,42,.85)); }
}
.mark__q:hover, .mark__q:focus-visible {
  filter: brightness(1.75) saturate(1.35)
          drop-shadow(0 0 34px rgba(255,150,60,.92));
  z-index: 3;
}
.mark.is-armed .mark__q { transition: filter .5s var(--ease-out), transform .5s var(--ease-out); }
/* Hover draws the brick OUT of the wall along its own axis — a scaled brick
   reads as a picture zooming; a displaced one reads as stone being lifted. */
.mark.is-armed .mark__q:hover,
.mark.is-armed .mark__q:focus-visible {
  /* OUTWARD, along the brick's own extrusion axis. The sign was inverted, so
     hovering the bottom-right brick slid it UP-LEFT across its neighbours and
     sheared the wordmark in half — visible immediately in a render, invisible
     in the rule itself. */
  transform: translate3d(calc(var(--dx) * 6px), calc(var(--dy) * 6px), 30px);
  z-index: 4;
}

/* THE CALLOUT. John: "when you pass the mouse above, a line that extends
   OUTSIDE must describe what it is." Each quarter gets an annotation leader
   that draws itself out past the mark's corner and ends in a label, the way a
   part is called out on a technical drawing. A word centred underneath, which
   is what this used to be, told you nothing about WHICH quarter you were on. */
.mark__callout {
  position: absolute; inset: -52% -64%;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
  transition: opacity .26s var(--ease-out);
  z-index: 5;
}
.mark__callout.is-shown { opacity: 1; }
.mark__callout svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.mark__callout .lead {
  fill: none; stroke: var(--ember); stroke-width: 1.3;
  stroke-dasharray: 400; stroke-dashoffset: 400;
  filter: drop-shadow(0 0 6px rgba(255,122,42,.8));
}
.mark__callout.is-shown .lead { animation: draw-lead .4s var(--ease-out) forwards; }
@keyframes draw-lead { to { stroke-dashoffset: 0; } }
.mark__callout .node { fill: var(--ember-hot); filter: drop-shadow(0 0 6px var(--ember)); }

/* An explicit width, because an absolutely-positioned box with only `left` set
   shrinks to fit the space REMAINING in its containing block — which here is
   6.5% of it, and the description wrapped to one word a line. */
.mark__tag {
  position: absolute; width: 16.5rem; white-space: nowrap;
  opacity: 0; transform: translateY(5px);
  transition: opacity .3s var(--ease-out) .14s, transform .3s var(--ease-out) .14s;
}
.mark__tag b {
  display: block;
  font-family: var(--sans); font-size: .7rem; font-weight: 700;
  letter-spacing: .24em; text-transform: uppercase; color: var(--ember-hot);
  text-shadow: 0 2px 14px #000, 0 0 24px rgba(0,0,0,.95);
}
/* A plate behind the label. The callout floats over a moving picture AND, for
   the top-right quarter, over the navigation — the label was rendering straight
   through "OREML" and "ABOUT US". The chrome is dimmed while a callout is up
   (see .chrome.is-dimmed) and the plate carries the rest. */
.mark__tag {
  /* ⛔ NO backdrop-filter ANYWHERE ON THIS SITE — this note covers all four
     places that had one (this plate, the sound button, the film end card, and
     the music credit that appears during the very first seconds, which is the
     worst possible timing because the terrain bake and the shader compiles land
     there too). It was blur(6px) here, and a backdrop filter forces
     the compositor to read back the pixels BEHIND the element every frame it is
     composited — over a full-screen WebGL canvas that is a per-frame copy of the
     whole scene, on the one surface that is already the most expensive thing on
     the page. The plate below is opaque enough to do the same legibility job
     with no readback at all. */
  background: linear-gradient(100deg, rgba(5,2,3,.96), rgba(5,2,3,.86) 72%, rgba(5,2,3,.34));
  padding: .5rem .8rem .55rem;
}
.mark__tag[data-side='right'] {
  background: linear-gradient(260deg, rgba(5,2,3,.96), rgba(5,2,3,.86) 72%, rgba(5,2,3,.34));
}
/* ⛔ THE NAVIGATION IS NEVER DIMMED AND NEVER COVERED. John, twice: the tabs
   "disappear" when a quarter is hovered, and again while a film plays. The first
   was mine — a .18 opacity added so a callout label would not fight the nav for
   pixels. That was the wrong lever: the callout now carries an opaque plate of
   its own, so the plate solves the collision and the nav can stay at full
   strength. The second is z-order: the film overlay sits at z-index 40 and the
   chrome sat below it, so a film covered the only way off the page.
   A visitor must always be able to leave. */
.chrome nav.tabs { opacity: 1; }
.chrome { z-index: 60; }
.mark__tag i {
  display: block; font-style: normal; font-family: var(--serif);
  font-size: .82rem; letter-spacing: .01em; line-height: 1.45;
  color: var(--bone); margin-top: .3rem; white-space: normal;
  /* ⛔ NO max-width HERE. John: "bad lining". The label <b> is a block filling
     the tag's 15.5rem, while this carried max-width: 22ch — about 143px — so
     the two children were different widths and text-align gave them DIFFERENT
     EDGES: on a left-side callout the blurb right-aligned to 143px and the
     label to 248px, which is precisely the ragged mismatch in the screenshot.
     One box width for both children is the whole fix; the tag's own width is
     what sets the measure. */
  text-shadow: 0 2px 14px #000, 0 0 24px rgba(0,0,0,.95);
}
/* The call to action rides the leader line itself. It sits on the tag's own
   edge — the end the line arrives at — so it reads as a label ON the line
   rather than a fifth piece of floating chrome. */
.mark__tag .mark__cta {
  display: block; text-decoration: none; margin-top: .45rem;
  font-family: var(--sans); font-size: .58rem; font-weight: 700;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--ember-hot); opacity: .92;
}
/* The arrow points AT THE MARK, which is the thing being invited. A left-side
   callout sits to the LEFT of the quarter, so its arrow points right — the
   first version had both of them pointing away from the thing to click, which
   is an instruction that argues with itself. */
.mark__tag[data-side='left'] .mark__cta::after  { content: ' ▸'; opacity: .8; }
.mark__tag[data-side='right'] .mark__cta::before { content: '◂ '; opacity: .8; }
@media (prefers-reduced-motion: no-preference) {
  .mark__callout.is-shown .mark__cta { animation: ctaPulse .85s var(--ease-out) infinite; }
}
/* ⛔ FASTER AND HARDER. John: "click me blink inside the cards must be blinking
   faster and more robust." The old pulse was 1.9 s between .55 and 1 opacity —
   a slow breath, which reads as ambient decoration rather than as an
   instruction, and at .55 it never actually looked OFF. 0.85 s, and it goes to
   .18 so the eye registers a real blink; the ember glow and a hair of weight
   ride with it so the cue survives being over a bright frame of the film. */
@keyframes ctaPulse {
  0%, 100% { opacity: .18; text-shadow: none; }
  45%      { opacity: 1;   text-shadow: 0 0 10px rgba(255,122,42,.85), 0 0 22px rgba(255,60,20,.45); }
}

.mark__callout.is-shown .mark__tag { opacity: 1; transform: none; }
/* ⛔ THE CALLOUT IS NOT REMOVED ON A PHONE — it is re-laid-out. It used to be
   display:none below 760px, which deleted the only text that says what the four
   quarters ARE. A phone visitor saw a logo, four silent tiles and a motto.
   The leader line is what cannot survive: it is drawn in percentages across a
   viewport that is now taller than it is wide, so it would cross the mark. The
   line goes; the words stay, as a card pinned under the mark. */
@media (max-width: 760px) {
  .mark__callout svg { display: none; }
  /* ⛔ THE CONTAINER, NOT JUST THE LINE. Hiding the SVG left its parent at the
     size the desktop leader needs — measured 547px wide, from x=-79 to x=469 in a
     390px viewport — which is a horizontal scrollbar on every phone, produced by
     an element that draws nothing. The card inside is positioned absolutely and
     still places itself freely. */
  .mark__callout { left: 0; right: 0; width: 100%; }
  /* Positioned with left/right rather than a centring transform, because
     `.mark__callout.is-shown .mark__tag` sets `transform: none` to land the
     entry animation — a translateX(-50%) here would be cancelled by it the
     instant the card became visible, which is exactly the half-off-screen card
     the first attempt produced. */
  /* ⛔ !important ON THE GEOMETRY, DELIBERATELY. The desktop callout is placed
     by inline percentages derived from the leader's elbow, and although those are
     now suppressed on touch, the base rule still carries `position:absolute` plus
     a fixed width from a selector of equal specificity later in the file. Without
     these the card rendered ON TOP OF THE MARK — covering the very thing it was
     describing. The card is pinned to the bottom edge instead, where it can never
     cover the logo whatever the viewport does. */
  /* ⛔ NOT position: fixed — MEASURED, and the reason is worth writing down.
     `fixed` was resolving against an ANCESTOR, not the viewport: the mark's
     assembly animation leaves a transform on a parent, and a transformed ancestor
     becomes the containing block for any fixed-position descendant. So
     `left: 20px` computed as exactly that and still rendered at x=95, and the
     card sat squarely on top of the logo it was describing while every computed
     value looked correct. The card is anchored to the mark instead, which is what
     it belongs to, and placed just under it.
     transform carries !important because `.mark__callout.is-shown .mark__tag`
     sets `transform: none` to land the entry animation and would re-centre it
     back off-axis. */
  .mark__callout .mark__tag {
    position: absolute !important;
    top: calc(100% + .9rem) !important;
    bottom: auto !important;
    left: 50% !important; right: auto !important;
    transform: translateX(-50%) !important;
    width: min(21rem, calc(100vw - 2 * var(--gut))) !important;
    max-width: none !important;
    text-align: left !important;
    background: linear-gradient(180deg, rgba(5,2,3,.95), rgba(5,2,3,.985));
    border-left: 2px solid var(--ember);
    box-shadow: 0 14px 40px rgba(0,0,0,.7);
  }
  /* Room under the mark for the card, so it never reaches the motto either. */
  .hero__foot { padding-top: 9.5rem; }
  .mark__tag[data-side='left'] .mark__cta::after,
  .mark__tag[data-side='right'] .mark__cta::before { content: ''; }
  /* Tap, not click. */
  /* Specificity has to beat `.mark__tag[data-side='…'] .mark__cta::after`, which
     is one class-token higher and was blanking this. */
  .mark__tag[data-side] .mark__cta::after { content: ' — tap again' !important; opacity: .7; }
}

/* A finger needs a target it can actually hit. */
@media (pointer: coarse) {
  .mark__q { min-width: 44px; min-height: 44px; }
}

/* ⛔ THE NAVIGATION MUST FIT. On a 390-wide screen the three tabs plus the sound
   button overflowed and "CONTACT" was cut off at the edge — the last item in the
   list, so the page silently lost its contact link on exactly the devices most
   likely to want it. Nothing is removed: the type and the gaps come down, and
   the brand's descriptor drops out of the masthead (it is still in the footer on
   every page), which is what buys the room. */
@media (max-width: 560px) {
  .chrome { padding-left: .75rem; padding-right: .75rem; }
  .chrome nav.tabs { gap: .85rem; }
  .chrome nav.tabs a { font-size: .62rem; letter-spacing: .12em; }
  .chrome .brand span, .chrome .brand b { font-size: .74rem; letter-spacing: .12em; }
  #soundBtn { width: 34px; height: 34px; margin-left: .15rem; }
}
@media (max-width: 380px) {
  .chrome .brand { display: none; }        /* the mark itself is the brand here */
}

.hero__foot { text-align: center; display: grid; gap: .75rem; justify-items: center; }
/* `h1.motto`, not `.motto`: the element also carries `.display`, whose
   `h1.display` rule is one specificity point higher and was winning — the
   headline rendered at 6.5rem and ran off the bottom of the viewport. */
h1.motto, .motto {
  font-family: var(--disp); font-weight: 800; text-transform: uppercase;
  font-size: clamp(1.05rem, 2.35vw, 2.1rem);
  letter-spacing: .13em; line-height: 1.34; margin: 0 auto;
  max-width: 26ch; text-align: center;
  text-shadow: 0 2px 30px rgba(0,0,0,.96);
}
.motto .l2 { display: block; }
/* ONE LINE. It is a single statement with a mid-point pivot, and breaking it
   across two lines splits the two halves of the claim apart. The size scales
   with the viewport so it stays on one line down to a phone, where it is
   allowed to wrap because 46 characters cannot fit in 390 pixels. */
.subcopy {
  margin: 0 auto; max-width: none; text-align: center;
  font-family: var(--serif); font-size: clamp(.62rem, 1.12vw, 1.02rem);
  letter-spacing: .012em; text-transform: none; color: var(--bone-dim);
  line-height: 1.5; text-shadow: 0 2px 18px rgba(0,0,0,.92);
  white-space: nowrap;
}
@media (max-width: 700px) { .subcopy { white-space: normal; max-width: 40ch; font-size: .8rem; } }
.subcopy .sep { color: var(--ember); padding: 0 .5em; }

.enter-hint {
  display: inline-flex; align-items: center; gap: .6rem;
  font-family: var(--mono); font-size: .7rem; letter-spacing: .24em;
  text-transform: uppercase; color: var(--bone-dim);
}
.enter-hint .dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--ember);
  box-shadow: 0 0 10px var(--ember);
  animation: breathe 2.4s var(--ease-in-out) infinite;
}
@keyframes breathe { 0%,100% { opacity: .35; transform: scale(.8);} 50% { opacity: 1; transform: scale(1.25);} }

/* ═══ FILM MODE: captions, progress, exit ═══════════════════════════════ */

.film {
  position: fixed; inset: 0; z-index: 40;
  /* ⛔ DECLARE THE COLUMN. The grid declared rows only, so its column track was
     IMPLICIT and therefore auto-sized to its widest item, with the default
     justify-content: start leaving the rest of the viewport outside the grid
     entirely. The caption band's `justify-self: stretch` then stretched it to
     that track rather than to the screen — measured at [48, 882] of a [48, 1392]
     film, i.e. a full-bleed scrim that stopped with a hard vertical edge two
     thirds across. `grid-column: 1 / -1` did not help and could not: spanning
     every column of a grid whose columns are content-width still gives you
     content width. minmax(0, 1fr) is what makes the track the container. */
  display: grid; grid-template-rows: auto 1fr auto; grid-template-columns: minmax(0, 1fr);
  /* The film is full-bleed and fixed, so both edges need the same treatment as
     .chrome — otherwise the subtitles run under the home indicator. */
  padding:
    max(clamp(4.6rem, 11vh, 7rem), calc(env(safe-area-inset-top, 0px) + 3.4rem))
    max(var(--gut), env(safe-area-inset-right, 0px))
    max(clamp(1.4rem, 4vh, 2.6rem), calc(env(safe-area-inset-bottom, 0px) + .8rem))
    max(var(--gut), env(safe-area-inset-left, 0px));
  pointer-events: none;
  opacity: 0; visibility: hidden;
  transition: opacity .55s var(--ease-out), visibility .55s;
}
.film.is-open { opacity: 1; visibility: visible; }
/* ⛔ THE FOOTER IS PAGE FURNITURE, NOT PART OF THE FILM. It sits in the normal
   flow below the hero, so during a film it showed through the fixed overlay and
   the caption band landed straight on top of it. A film is full-bleed; nothing
   from the page underneath belongs in the frame. */
body.film-mode .foot { opacity: 0; pointer-events: none; }
.foot { transition: opacity .45s var(--ease-out); }
.film > * { pointer-events: auto; }

/* Row 1 explicitly. Its siblings all declare a row, so auto-placement pushed
   this one past them and the film's name rendered at the bottom of the screen,
   under the caption it was supposed to sit above. */
.film__title {
  /* ⛔ COLUMN 1 EXPLICITLY. .film__bar also declares grid-row: 1, so with the
     column left to auto-placement the two of them occupied DIFFERENT implicit
     columns — which is what kept the caption band short even after the column
     track was declared: measured caption [48, 1248] against a film [0, 1440],
     with the bar sitting alone in a second track from 1248. Both are positioned
     by justify-self, not by living in separate columns. */
  grid-row: 1; grid-column: 1;
  justify-self: start;
  align-self: center;
  font-family: var(--mono); font-size: .74rem; letter-spacing: .3em;
  text-transform: uppercase; color: var(--ember);
  opacity: 0; transform: translateY(-8px);
  transition: opacity .6s var(--ease-out) .2s, transform .6s var(--ease-out) .2s;
}
.film.is-open .film__title { opacity: 1; transform: none; }

/* the caption band — subtitles, the film's dialogue */
/* ⛔ SUBTITLES, NOT TEXT FLOATING IN THE SHOT. John: the narration "must appear
   somewhere that does not interfere with the animation", at one size and one
   colour. Three things were wrong and all three are fixed here.
   1 · it sat in a soft radial fade over the middle of the frame, so the letters
       competed with whatever the robots were doing behind them;
   2 · legibility came from a heavy dark halo on the glyphs — the "outline" — the
       standard tell of text that has no plate under it;
   3 · the two caption styles disagreed on family, SIZE and colour, so the voice
       of the film changed line to line.
   It is now one band, pinned to the bottom edge, with a real plate. */
.film__caption {
  /* ⛔ SPAN EVERY COLUMN. justify-self: stretch only fills the column the item is
     IN, and .film's grid declares rows but not columns — so the implicit column
     track the band landed in was 834px of a 1440px viewport, and the "full-width"
     scrim stopped two thirds of the way across with a visible vertical edge.
     Measured: caption [48, 882] against a film padded to [48, 1392]. */
  grid-row: 3; grid-column: 1 / -1; justify-self: stretch; text-align: center;
  padding: .85rem clamp(1rem, 4vw, 2.4rem) .9rem;
  /* ⛔ A SCRIM, NOT A SLAB — now that the glyphs carry their own stroke.
     The opaque plate existed to make un-outlined text legible over a moving
     picture, and it is what John saw as a black box. With the outline doing that
     job the backing only has to lift the contrast a little, so it fades to
     nothing at the top and never reaches full opacity. The film stays visible
     THROUGH the narration, which is the point of putting narration over a film. */
  background: linear-gradient(180deg, rgba(6,3,4,0) 0%, rgba(6,3,4,.34) 42%, rgba(6,3,4,.62) 100%);
  border-top: none;
  width: 100%; max-width: none;
  min-height: 5.2rem;
  display: grid; align-content: end; justify-items: center; gap: .35rem;
  /* ⛔ THE PLATE ONLY EXISTS WHILE THERE IS A LINE ON IT. John: "what the hell
     are those empty black boxes". Giving the band a real background so the
     narration would stop needing a halo also gave it a background BETWEEN
     lines — a wide black slab sitting across the bottom of the film with
     nothing in it, which is worse than the problem it solved. The band keeps
     its reserved height so captions never make the frame jump, and shows its
     plate only when it is carrying text. */
  opacity: 0;
  transition: opacity .28s var(--ease-out);
  pointer-events: none;
}
.film__caption.is-live { opacity: 1; }
.caption__speaker {
  font-family: var(--mono); font-size: .64rem; letter-spacing: .3em;
  text-transform: uppercase; color: var(--ember); opacity: .85;
}
/* ONE voice: one family, one size, one colour, whatever the beat.
   ⛔ AND A REAL OUTLINE, WHICH IS NOT THE SAME THING AS THE ONE THAT WAS REMOVED.
   John asked twice, and the two asks are not contradictory once you separate the
   technique from the craft. What he rejected was a blurred BLACK HALO —
   `text-shadow: 0 3px 26px #000, 0 0 60px #000` — which is not an outline at
   all: it is a soft shadow bleeding several pixels out of every glyph, muddying
   the counters and thickening the stems unevenly. What he asked for after that
   is "beautiful outlines": a real stroke.
   So this is a STROKE, drawn BEHIND the fill via paint-order, which is the whole
   trick — the default paints the stroke over the letterform and eats half its
   weight, which is why naive text-stroke always looks clumsy. Behind the fill,
   the glyph keeps its designed shape and gains a hairline of definition. The
   stroke is warm near-black rather than pure black so it reads as depth against
   an ember-lit scene instead of as a sticker, and a single tight shadow seats
   the line without haloing it. */
.caption__line, .caption__line--soft {
  font-family: var(--serif); font-weight: 400; text-transform: none;
  font-size: clamp(1.02rem, 1.55vw, 1.28rem); line-height: 1.42;
  letter-spacing: .01em; color: var(--bone);
  paint-order: stroke fill;
  -webkit-text-stroke: 2.4px rgba(9, 4, 4, .88);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  max-width: min(52rem, 90vw);
  margin: 0;
}
/* The speaker label is the same treatment at its own weight, so the two lines
   read as one object rather than as text plus a caption. */
.caption__speaker {
  paint-order: stroke fill;
  -webkit-text-stroke: 2px rgba(9, 4, 4, .85);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
}
/* The film's own title gets it too — it sits over the picture with nothing
   behind it at all, which is exactly where an outline earns its place. */
.film__title {
  paint-order: stroke fill;
  -webkit-text-stroke: 2.2px rgba(9, 4, 4, .88);
}
/* each caption wipes in from the left, like hot metal being drawn out */
.caption__line, .caption__speaker { opacity: 0; }
.film__caption.is-live .caption__line,
.film__caption.is-live .caption__speaker {
  /* ⛔ 0.28 s, NOT 0.55. The wipe is spent BEFORE the line is fully legible, so it
     comes straight out of reading time — at the old 0.55 s a caption holding
     1.8 s gave the eye about 1.25 s of readable text. Halving it hands every
     caption on the site a third of a second back for free, and the gesture still
     reads as drawn-out metal rather than as a cut. */
  animation: wipe-in .28s var(--ease-out) forwards;
}
.film__caption.is-live .caption__line { animation-delay: .04s; }
@keyframes wipe-in {
  from { opacity: 0; transform: translateY(14px); clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; transform: none;             clip-path: inset(0 0 0 0); }
}

/* the film's own controls */
.film__bar {
  grid-row: 1; grid-column: 1; justify-self: end; display: flex; align-items: center; gap: .9rem;
}
.film__progress {
  width: clamp(4rem, 14vw, 9rem); height: 2px;
  background: rgba(243,236,232,.16); overflow: hidden;
}
.film__progress i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--blood), var(--ember-hot)); }
.film__exit {
  background: none; border: 0; cursor: pointer; color: var(--bone-dim);
  font-family: var(--mono); font-size: .68rem; letter-spacing: .22em; text-transform: uppercase;
  padding: .5rem; transition: color .25s var(--ease-out);
}
.film__exit:hover { color: var(--ember); }

/* the end card that closes each film */
/* A scrim. These words sit over a moving picture whose luminance is not under
   their control — measured on the reduced-motion path, where the end card lands
   over a lit mountain ridge and the body copy stopped being readable. A text
   shadow alone does not survive a bright backdrop; a soft ground does. */
/* IT MUST NOT COVER THE PICTURE. John: "these messages must not cover our
   beautiful animation — must be clear but in some corner." A centred panel over
   the middle of a shot hides the thing the shot was made for. It now sits low
   and left as a narrow card with a rule down its edge, so the film keeps the
   frame and the words keep their contrast. */
.film__end {
  grid-row: 2; align-self: end; justify-self: start; text-align: left;
  margin: 0 0 clamp(.5rem, 1.5vh, 1.2rem) 0;
  padding: clamp(.95rem, 2.1vw, 1.4rem) clamp(1.05rem, 2.3vw, 1.6rem);
  background: linear-gradient(100deg, rgba(5,2,3,.94) 0%, rgba(5,2,3,.87) 60%, rgba(5,2,3,.60) 100%);
  border-left: 2px solid var(--ember);
  display: grid; gap: .8rem; justify-items: start;
  max-width: min(26rem, 76vw);
  opacity: 0; visibility: hidden; transform: translateY(18px);
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out), visibility .7s;
}
.film__end.is-shown { opacity: 1; visibility: visible; transform: none; }
.film__end p { margin: 0; color: var(--bone-dim); }
.film__end .actions { display: flex; flex-wrap: wrap; gap: .7rem; justify-content: flex-start; }
.film__end h2.display { font-size: clamp(1.1rem, 1.9vw, 1.5rem); }
.film__end p { font-size: .92rem; line-height: 1.55; }

/* ═══ THE MUSIC CREDIT — small, low corner, 3 s, then it withdraws ══════ */

/* THE SCORE'S CREDIT. It was a wall of monospace with a raw URL in it — John
   called it ugly and he was right. It is now a small plate: a hairline rule, the
   track and its composer in the site's own type, and the licence code set small
   underneath as the reference it is. It appears bottom-RIGHT so it never sits
   under the callout leaders or the end card, holds for three seconds, and goes. */
.credit {
  /* Same reasoning as .chrome: this sits on the bottom edge, which on a modern
     iPhone is exactly where the home indicator lives. */
  position: fixed;
  right: max(var(--gut), env(safe-area-inset-right, 0px));
  bottom: max(clamp(1rem, 3vh, 1.8rem), calc(env(safe-area-inset-bottom, 0px) + .6rem));
  z-index: 70;
  max-width: min(19rem, 72vw);
  padding: .7rem .9rem .72rem;
  background: linear-gradient(180deg, rgba(9,4,6,.93), rgba(5,2,3,.88));
  border: 1px solid rgba(243,236,232,.10);
  border-top: 1px solid rgba(255,122,42,.36);
  text-align: right;
  opacity: 0; transform: translateY(10px);
  visibility: hidden; pointer-events: none;
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out), visibility .7s;
}
.credit .k {
  display: block; font-family: var(--sans); font-size: .58rem; font-weight: 700;
  letter-spacing: .26em; text-transform: uppercase; color: var(--ember);
  margin-bottom: .28rem;
}
.credit .t {
  display: block; font-family: var(--serif); font-size: .92rem; line-height: 1.35;
  color: var(--bone);
}
.credit .t em { font-style: italic; color: var(--bone); }
.credit .w { display: block; font-family: var(--serif); font-size: .78rem; color: var(--bone-dim); margin-top: .1rem; }
.credit .c {
  display: block; font-family: var(--mono); font-size: .54rem; letter-spacing: .1em;
  color: var(--ash-dim); margin-top: .42rem;
}
.credit.is-shown { opacity: 1; transform: none; visibility: visible; pointer-events: auto; }
.credit a { color: var(--ember-hot); text-decoration: none; border-bottom: 1px solid rgba(255,179,71,.28); }
.credit a:hover { border-bottom-color: var(--ember-hot); }

/* ═══ CONTENT PAGES (about / oreml / contact) ═══════════════════════════ */

.page {
  position: relative; z-index: 10;
  max-width: var(--max); margin: 0 auto;
  padding: clamp(7rem, 18vh, 11rem) var(--gut) clamp(4rem, 10vh, 7rem);
}
.page > * + * { margin-top: 1.15rem; }
.page section { margin-top: clamp(2.6rem, 7vh, 4.5rem); }
.page h2.display { margin-bottom: .9rem; }
.page h3 { font-size: 1.05rem; letter-spacing: .04em; margin: 2rem 0 .4rem; color: var(--bone); }

/* a quiet backdrop for the reading pages — the forge, banked down */
/* ⛔ NEVER A CSS filter ON A LIVE WebGL CANVAS.
   This was `filter: brightness(.34) saturate(.72)` on #stage — full screen, on a
   surface that produces a new frame sixty times a second. A filter is not a
   paint colour: the compositor has to take the finished canvas, run a shader
   over every pixel of it, and composite the RESULT, every single frame. On a
   reading page that doubles the cost of a backdrop nobody is looking at, and it
   is the second machine-independent reason About and Contact ran hot.
   A flat overlay does the same job for nothing — it is one more rectangle in a
   layer the compositor was already going to draw. The saturation loss is taken
   in the render instead (page.js lowers exposure and saturation on the grade),
   which is free because it happens inside a pass that already exists. */
body.page-mode #stage::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(180deg, rgba(3,1,2,.72) 0%, rgba(3,1,2,.60) 45%, rgba(3,1,2,.78) 100%);
}
body.page-mode #poster { opacity: .55; }
/* ⛔ AND THE HAND-OFF HAS TO OUT-SPECIFY IT. `body.page-mode #poster` is (0,1,1,1)
   and `#poster.is-gone` is (0,1,1,0), so the .55 above WON and page.js's own
   hand-off was a no-op — measured: a poster carrying class `is-gone` computed to
   opacity 0.55. The .55 is not lowered, because other surfaces rely on it; this
   rule is narrower and only applies once the canvas has actually taken over. */
body.page-mode #poster.is-gone { opacity: 0; }
/* ⛔ AND WHEN THE POSTER *IS* THE BACKDROP, IT IS NOT DIMMED.
   The .55 above exists for one situation: a live canvas is rendering behind the
   poster and the poster is a veil over it. On a machine that scores low — or one
   that prefers reduced motion — there IS no canvas, the poster is the backdrop
   itself, and dimming it to 55% over black just makes the page darker for the
   visitor whose hardware already gives them the least. `.is-revealed` means "I am
   the backdrop"; `.is-gone` means "the canvas took over". Both out-specify the
   blanket rule, and they are mutually exclusive by construction. */
body.page-mode #poster.is-revealed { opacity: 1; }

/* ⛔ ON A NO-STAGE PAGE THE POSTER IS NOT A FALLBACK, IT IS THE BACKDROP.
   The legal pages ship without a <canvas>, so nothing ever adds .is-revealed and
   nothing ever fades the poster up — without this they would render on the flat
   #000 the poster starts at, i.e. the site's own legal pages would be the only
   two that look unfinished. The same gradient the WebGL scene fades in behind is
   painted here directly, at full strength rather than the .55 that exists to let
   a live canvas show through a poster that is no longer needed.
   Same specificity as the rule above (one id, one class), so this must stay
   AFTER it — source order is what decides, and moving it up would silently
   restore the dimming. */
body.no-stage #poster {
  opacity: 1;
  background:
    radial-gradient(46% 34% at 50% 86%, rgba(255,122,42,.30) 0%, rgba(120,16,22,.12) 44%, transparent 74%),
    radial-gradient(120% 95% at 50% 100%, #0b0406 0%, #050203 54%, #010001 100%);
}

.rule { height: 1px; background: linear-gradient(90deg, var(--blood), transparent 62%); border: 0; margin: 2.6rem 0; }

.grid-2 { display: grid; gap: clamp(1.4rem, 4vw, 2.6rem); grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }

.card {
  padding: clamp(1.2rem, 3vw, 1.8rem);
  background: linear-gradient(160deg, rgba(29,18,21,.72), rgba(10,5,7,.6));
  border: 1px solid rgba(243,236,232,.09);
  border-top: 1px solid rgba(255,122,42,.28);
}
.card h3 { margin-top: 0; }
.card p:last-child { margin-bottom: 0; }

.stat { font-family: var(--disp); font-size: clamp(2.4rem, 6vw, 4rem); line-height: 1; }

/* ═══ FORM ═════════════════════════════════════════════════════════════ */

form#contact { display: grid; gap: 1.05rem; max-width: 40rem; margin-top: 2rem; }
form#contact label { display: grid; gap: .4rem; font-size: .74rem; letter-spacing: .2em; text-transform: uppercase; color: var(--bone-dim); font-weight: 600; }
form#contact input, form#contact select, form#contact textarea {
  font: inherit; font-size: 1rem; letter-spacing: normal; text-transform: none;
  color: var(--bone); background: rgba(8,4,5,.66);
  border: 1px solid rgba(243,236,232,.16); border-radius: 0;
  padding: .78rem .9rem; width: 100%;
  transition: border-color .25s var(--ease-out), background .25s var(--ease-out);
}
form#contact textarea { resize: vertical; min-height: 8.5rem; line-height: 1.6; }
form#contact input:focus, form#contact select:focus, form#contact textarea:focus {
  border-color: var(--ember); background: rgba(14,7,9,.85); outline: none;
}
form#contact select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--ember) 50%), linear-gradient(135deg, var(--ember) 50%, transparent 50%); background-position: calc(100% - 18px) 1.25rem, calc(100% - 13px) 1.25rem; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }
form#contact button[type='submit'] { justify-self: start; margin-top: .4rem; }

/* the honeypot: hidden from people AND from assistive tech; bots fill it. */
.hp { position: absolute !important; left: -9999px !important; width: 1px; height: 1px; overflow: hidden; }

#status { min-height: 1.5rem; font-size: .9rem; margin: 0; }
#status[data-state='ok']    { color: var(--ember-hot); }
#status[data-state='error'] { color: var(--blood-hot); }

/* ═══ FOOTER ═══════════════════════════════════════════════════════════ */

/* THE FOOTER carries three things and nothing else, and they are set to be
   READ — the previous row was six small grey items of equal weight, which is
   how a footer becomes wallpaper. */
.foot {
  position: relative; z-index: 10;
  border-top: 1px solid rgba(243,236,232,.09);
  padding: clamp(2rem, 5vh, 3.2rem) var(--gut);
  display: grid; gap: .9rem;
  color: var(--bone-dim);
}
/* John: the brand sentence on the LEFT, the three links on the RIGHT. A plain
   flex row put all four items in one left-packed run, so the identity and the
   navigation read as one undifferentiated list. The brand takes the free space
   and the links close up against the right edge. */
.foot__row {
  /* ⛔ NOT max-width: var(--max). The header's brand starts at the page gutter
     (x=48 at 1440) while this row was constrained to the 68rem reading rail and
     centred, starting at x=176 — so the same word sat in two different places on
     one screen. The row now uses the page's own gutter, which is what makes the
     left and right margins match each other AND match the masthead. */
  display: flex; flex-wrap: wrap; gap: .8rem clamp(1.1rem, 3vw, 2.4rem);
  align-items: baseline; max-width: none; margin: 0; width: 100%;
}
.foot__row .foot__brand { margin-right: auto; }
@media (max-width: 680px) {
  /* Below this the two groups cannot share a line without the links wrapping
     mid-group, which looks worse than stacking them deliberately. */
  .foot__row { flex-direction: column; align-items: flex-start; }
  .foot__row .foot__brand { margin-right: 0; }
}
/* ONE LINE: the name and what the company does belong in the same sentence,
   not stacked. The descriptor sits inline after a separator. */
.foot__brand {
  font-family: var(--disp); font-weight: 700; text-transform: uppercase;
  letter-spacing: .17em; font-size: .92rem; color: var(--bone);
  /* ⛔ nowrap. John: "one line here also", and with three spans instead of two
     the default flex-wrap broke the brand onto a SECOND line at every width
     including 1440 — measured, four items at two distinct baselines. The row
     around it still wraps, so below the stacking breakpoint the groups separate
     cleanly; it is only the sentence itself that must not come apart. */
  display: inline-flex; align-items: baseline; gap: .55rem; flex-wrap: nowrap;
}
@media (max-width: 900px) {
  /* Under this the sentence genuinely cannot hold one line at a readable size,
     so it is allowed to wrap rather than to shrink or to overflow. */
  .foot__brand { flex-wrap: wrap; }
}
/* ⛔ THE SEPARATOR IS CSS, NOT TYPED INTO THE MARKUP. Three facts now share one
   line — the copyright, what the company does, and the reservation of rights —
   and John asked for both of the last two after asking for the first. Putting a
   literal "·" in the HTML means every page has to agree about it and a future
   edit can drop one; generating it from the span's position cannot drift, and it
   disappears by itself if a span is ever removed. */
.foot__brand span + span::before,
.foot__brand > span:first-of-type::before {
  content: '·';
  margin-right: .45rem;
  color: var(--bone-dim);
  opacity: .5;
}
.foot__brand span {
  /* The separator dot went with the descriptor it separated. "© 2026 OBSITH ·
     All rights reserved." reads as two facts joined by punctuation that means
     nothing; the sentence just continues. */
  display: inline; font-family: var(--serif); font-weight: 400; text-transform: none;
  letter-spacing: .01em; font-size: .88rem; color: var(--bone-dim);
}
.foot__brand { gap: .45rem; }
.foot__brand span::before { content: '·'; color: var(--ember); margin-right: .7rem; }
.foot__row a {
  font-family: var(--sans); font-size: .78rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--bone-dim);
  border-bottom: 1px solid rgba(243,236,232,.16); padding-bottom: .12rem;
}
.foot__row a:hover { color: var(--ember-hot); border-bottom-color: var(--ember); }
.foot__row a.mail { text-transform: none; letter-spacing: .02em; font-family: var(--serif); font-size: .92rem; }
.foot a { color: var(--ash); text-decoration: none; border-bottom: 1px solid transparent; transition: color .25s, border-color .25s; }
.foot a:hover { color: var(--bone-dim); border-bottom-color: var(--ash-dim); }
.foot__credit { font-family: var(--mono); font-size: .64rem; color: var(--ash-dim); line-height: 1.6; }

/* ═══ RESPONSIVE + PREFERENCE ══════════════════════════════════════════ */

@media (max-width: 720px) {
  nav.tabs a { font-size: .68rem; letter-spacing: .1em; }
  .brand span { display: none; }
  .credit { max-width: calc(100vw - 2 * var(--gut)); font-size: .56rem; }
}

/* Reduced motion is honoured for real: the cinematic stops moving, the mark
   stops flying in, and the blink cycle becomes a static lit state. The site
   still WORKS — nothing is removed, only the motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .mark__q { opacity: 1 !important; transform: none !important; }
}

/* ═══ THE ABOUT PAGE — openings, a sequence, and numbers ═══════════════════
   John asked for "beautiful openings and a more interactive way of
   presentation". Everything below is decoration over markup that is already
   complete: with scripting off, `will-reveal` is never added and every section
   is simply visible, and the steps are simply open. Nothing here is the only
   route to a word on the page. */

.will-reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .9s var(--ease-out), transform .9s var(--ease-out);
}
.will-reveal.is-in { opacity: 1; transform: none; }
/* Children stagger, so a list unrolls rather than appearing all at once. The
   delay is on the CHILD, not the parent, so a long section does not wait. */
.will-reveal > * { transition-delay: 0ms; }
.will-reveal.is-in > *:nth-child(1) { animation: rise .8s var(--ease-out) both .00s; }
.will-reveal.is-in > *:nth-child(2) { animation: rise .8s var(--ease-out) both .07s; }
.will-reveal.is-in > *:nth-child(3) { animation: rise .8s var(--ease-out) both .14s; }
.will-reveal.is-in > *:nth-child(4) { animation: rise .8s var(--ease-out) both .21s; }
.will-reveal.is-in > *:nth-child(5) { animation: rise .8s var(--ease-out) both .28s; }
@keyframes rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

/* The opening line of the page: an inscription that draws itself. */
.opening { position: relative; padding-block: clamp(1rem, 4vh, 3rem); }
.opening h1 { position: relative; z-index: 1; }
.opening::after {
  content: ''; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: linear-gradient(90deg, var(--ember), transparent);
  transition: width 1.6s var(--ease-out) .3s;
}
.opening.is-in::after { width: min(34rem, 70%); }

/* ── the sequence ────────────────────────────────────────────────────────── */
.sequence { display: grid; grid-template-columns: 2px 1fr; gap: 0 clamp(1.1rem, 3vw, 2rem); margin-top: 2.4rem; }
.step-rail { position: relative; background: rgba(243,236,232,.10); }
.step-rail i {
  position: absolute; left: 0; top: 0; width: 100%; height: 25%;
  background: linear-gradient(180deg, var(--ember-hot), var(--blood));
  box-shadow: 0 0 12px rgba(255,122,42,.55);
  transition: height .5s var(--ease-out);
}
.steps { display: grid; gap: .2rem; }
.step {
  position: relative; cursor: pointer;
  padding: 1rem 1.1rem 1rem 1.4rem;
  border-left: 2px solid transparent;
  transition: background .35s var(--ease-out), border-color .35s var(--ease-out), opacity .35s var(--ease-out);
  opacity: .52;
}
.step:hover, .step:focus-visible { opacity: 1; }
.step.is-open {
  opacity: 1;
  background: linear-gradient(90deg, rgba(255,122,42,.07), transparent 70%);
  border-left-color: var(--ember);
}
.step h3 {
  margin: 0; font-family: var(--disp); font-weight: 700; text-transform: uppercase;
  font-size: clamp(.92rem, 1.5vw, 1.12rem); letter-spacing: .1em; color: var(--bone);
  display: flex; align-items: baseline; gap: .75rem;
}
.step h3 em {
  font-style: normal; font-family: var(--mono); font-size: .68rem; letter-spacing: .2em;
  color: var(--ember); flex: 0 0 auto;
}
/* The body is CLIPPED, not removed: it is in the document and reachable by a
   screen reader and by find-in-page whatever the open state is. */
.step p {
  margin: 0; max-width: 58ch;
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height .45s var(--ease-out), opacity .35s var(--ease-out), margin .45s var(--ease-out);
}
.step.is-open p { max-height: 14rem; opacity: 1; margin-top: .6rem; }

/* ── the numbers ─────────────────────────────────────────────────────────── */

/* Cards get depth on hover, so the page answers a pointer. */
.card { transition: transform .4s var(--ease-out), border-color .4s var(--ease-out), background .4s var(--ease-out); }
.card:hover {
  transform: translateY(-5px);
  border-top-color: var(--ember);
  background: linear-gradient(160deg, rgba(40,24,28,.80), rgba(12,6,8,.66));
}

@media (prefers-reduced-motion: reduce) {
  .will-reveal, .will-reveal.is-in > * { opacity: 1 !important; transform: none !important; animation: none !important; }
  .step p { max-height: none !important; opacity: 1 !important; margin-top: .6rem !important; }
  .card:hover { transform: none; }
}
