/* ═══════════════════════════════════════════════════════════════════════════
   CORE — tokens, reset, and the typographic system.

   The type is built from the three supplied references (Scene Social Agency /
   Agency You / The Art). What those three share is not a font, it is a
   grammar, and that grammar is what is rebuilt here:

     1. a DIDONE display face at enormous size, tracked slightly tight
     2. a tiny WIDE-TRACKED grotesk caps line — the "plate" — set against it,
        with hairline rules running out of it ("EST. 2020", "WEDDING — EVENTS")
     3. a SCRIPT word that crosses the display word rather than sitting under it
     4. warm paper, visible grain, and a single deep red used only on the script

   The scale contrast between (1) and (2) is the whole effect. Do not narrow it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── THE FALLBACK THAT MAKES THE FONT SWAP INVISIBLE ─────────────────────
   The hero was rendering in Georgia for the first second and then jumping
   to Fraunces. The jump is not the swap itself — a web font always arrives
   after first paint — it is that GEORGIA IS 10.5% WIDER THAN FRAUNCES at
   the hero's own settings. Measured, at 100px: Fraunces 1359.0px for
   "Websites made to be remembered", Georgia 1501.2px, Times New Roman
   1349.3px. Times is within 0.7%, and `size-adjust` closes the rest.

   So the stack no longer falls back to Georgia. It falls back to a face
   built from Times, scaled to Fraunces's own width, with the same line
   box (both measure 162px tall at 100px, so no ascent/descent override is
   needed). The swap still happens; there is simply nothing to see when it
   does — no reflow, no change of width, no second layout. */
@font-face {
  font-family: "Fraunces Fallback";
  src: local("Times New Roman"), local("Times"), local("Liberation Serif");
  size-adjust: 100.74%;
  font-display: swap;
}

:root {
  /* ── Ground ──────────────────────────────────────────────────────────
     Warm ivory, not white. Luxury here comes from paper, spacing and type;
     colour does almost nothing on purpose. */
  /* AGED, not clean. The previous ivory (#f5f2ea) read as a modern light
     website: warm, but with almost no stock to it. These are the same hue
     family taken two steps toward parchment — enough that the sheet reads
     as paper that has been kept somewhere, not far enough to be sepia,
     yellow or dirty. Re-measured across every text role after the change;
     nothing lost more than 0.3:1 and nothing fell below its floor. */
  --paper:    #f1ece2;
  --paper-2:  #f6f1e6;
  --paper-3:  #d5cdbd;
  --ink:      #171412;   /* warm near-black, never pure #000 */
  --ink-2:    #57524a;   /* 6.7:1 on paper, safe for body */
  /* Measured, not guessed. #8f897e (the brief's secondary) is 3.1:1 on
     paper, which fails AA for the small tracked caps, kinds and counters
     that were using it. This is the same hue taken to 4.9:1, so every one
     of those passes; the lighter value survives as --ink-soft for genuinely
     decorative, large-only use. */
  /* Was #6e6a61 (4.5:1 on flat paper), which was fine while the ground WAS
     flat. The atmosphere darkens it to as low as 0.74 luma in places and
     that took every small role using this token to 3.8-4.3. Darkened one
     step: 6.1:1 on bare paper, 5.3:1 over the worst of the atmosphere. */
  --ink-3:    #5f5b53;
  --ink-soft: #8f897e;

  /* The specified accent. 6.6:1 on paper, so it is safe on the script
     lockup and on small caps, not only on display type. */
  --red:      #a83224;
  --red-deep: #8c2419;
  --brown:    #6b4f3a;

  --line:     rgba(23, 20, 18, 0.13);
  --line-2:   rgba(23, 20, 18, 0.07);
  --glass:    rgba(241, 236, 224, 0.72);

  /* Ink as opacity steps of ONE colour rather than a palette of greys.
     Hierarchy stays in tune because every level is literally the same ink. */
  --ink-82:   rgba(23, 20, 18, 0.82);   /* 11.4:1, the navigation index */
  --ink-70:   rgba(23, 20, 18, 0.70);   /* 8.0:1  */
  --ink-mid:  rgba(23, 20, 18, 0.62);   /* 4.9:1, the floor for small text */
  --ink-55:   rgba(23, 20, 18, 0.55);   /* 4.1:1, large text only */
  /* A very dark, very desaturated accent, for tinted surfaces. */
  --red-tint: #2a0f0a;
  --red-wash: rgba(168, 50, 36, 0.06);

  /* Surfaces. Nothing bleeds raw onto the ground: every visual sits in a
     panel with a radius and a hairline. */
  --r-sm:  0.75rem;
  --r-md:  1rem;
  --r-lg:  1.5rem;
  --r-full: 624rem;

  --blur-sm: 0.5rem;
  --blur-md: 0.75rem;
  --blur-lg: 1.5rem;

  /* ── Faces ───────────────────────────────────────────────────────── */
  /* The display face is FRAUNCES, and the reason is measured rather than
     stylistic. Bodoni Moda's hairlines rasterise to grey below weight 600
     on a light ground, which forced the whole site to 650 - and a Didone
     at 650 is a BOLD Didone. Its W paid for that worst of all: four thick
     diagonals crowding into one black mass at hero size.

     Fraunces is a high-contrast editorial serif whose thin strokes hold at
     420 with no dropout at any size on this ground, so the authority comes
     from the letterforms and not from weight, which is the whole brief.
     Its W is properly splayed with the middle apex stopping short of the
     cap line, its terminals are cut rather than rounded, and it carries a
     real italic companion. `opsz` drives the thick/thin contrast (144 at
     hero, 96 at head, 24 for small serif roles) and `WONK` releases the
     distinctive terminals at display size only.

     THE MARK IS NOT FRAUNCES. "site." is the approved wordmark and keeps
     its Bodoni, which is why one narrow instance of that family is still
     loaded. Refining the editorial voice is a refinement; changing the
     logo would be a rebrand. */
  --f-display: "Fraunces", "Fraunces Fallback", "Times New Roman", serif;
  --f-mark:    "Bodoni Moda", "Didot", "Bodoni MT", "Times New Roman", serif;
  --f-sans:    "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* THE SCRIPT IS ITALIANNO, NOT PINYON, AND THE REASON IS STROKE WEIGHT.
     Pinyon is a fine face at text size and a HEAVY one at display size:
     its downstrokes carry real mass, so the masthead read as a marker
     rather than a pen and the client's word for it was "chunky". Italianno
     is the same kind of mark — a formal, connected, copperplate hand — cut
     with far thinner strokes, and Herr Von Muellerhoff is finer still: a
     true hairline pen with long entry and exit strokes, which is what
     "written with a fine pencil" actually looks like.

     Chosen by rendering five candidates at both sizes on this ground
     rather than from their names (Italianno, Pinyon, Herr Von Muellerhoff,
     Petit Formal Script, Mrs Saint Delafield). Pinyon is unmistakably the
     heaviest of them at display size, which is the "marker" in the brief;
     Petit Formal is heavier still; Mrs Saint Delafield is fine but too
     flamboyant for a wordmark. Herr Von Muellerhoff is the only one that
     reads as a pencil line rather than a pen stroke.

     ONE script on the site, as before: the wordmark and the accent words
     share it, or the identity splits in two. Italianno and Pinyon stay in
     the stack as local fallbacks and are not requested. */
  --f-script:  "Herr Von Muellerhoff", "Italianno", "Pinyon Script", cursive;

  /* ── Scale ───────────────────────────────────────────────────────────
     Deliberately gapped. There is nothing between the plate (0.68rem) and
     the sub (1.2rem+) because the references have nothing there either. */
  /* Brought down from clamp(3.4rem, 10.6vw, 11rem). At 10.6vw the hero
     line set at 153px on a 1440 screen and filled the frame edge to edge,
     which reads as scale rather than as confidence. At 9vw it sets around
     130px, keeps its two lines, and leaves the paper around it visible,
     which is what the sheet is for. */
  --t-hero:    clamp(3.565rem, 10.35vw, 10.81rem);
  --t-display: clamp(2.875rem, 7.82vw, 8.05rem);
  --t-head:    clamp(2.185rem, 4.6vw, 4.255rem);
  /* THE SUPPORTING SCALE GETS ITS OWN LIFT, ON TOP OF THE GLOBAL 15%.
     The display sizes were the loud problem and they are fixed; the quiet
     one is that a lede at 18.8px under a 136px headline is not a hierarchy,
     it is a headline with a caption. These are the roles the client keeps
     calling "the grey text": the lede under a statement, the sub in a
     scene, body copy, and the tracked label. Each comes up between 8 and
     14%, and the DISPLAY sizes are deliberately untouched, so the gap
     between the loudest and the quietest type narrows rather than
     everything simply growing again. */
  --t-sub:     clamp(1.46rem, 2.36vw, 2.24rem);
  --t-lede:    clamp(1.32rem, 1.6vw, 1.66rem);
  --t-body:    clamp(1.18rem, 1.24vw, 1.32rem);
  --t-plate:   0.83rem;

  /* These are REGULAR weights, and that is the point. The old 650/600 were
     a workaround for Bodoni's dropout, not a design decision, and they are
     what made the site read as shouting. Measured across opsz 24-144 x
     wght 380-520 on this ground: Fraunces holds its hairlines everywhere
     in that range, so display type sits at 420 and heads at 460, and
     nothing on this site needs a bold weight to carry authority.

     Do not raise these to add impact. Impact comes from size, space and
     the letterforms; weight is the one lever that cheapens all three. */
  --w-display: 420;
  --w-head:    460;

  /* One place to tune the variable axes, so a size never gets the wrong
     optical master. High opsz is maximum thick/thin contrast, for type big
     enough to carry it. WONK 1 releases the splayed W and the cut
     terminals; below head size they become noise, so it goes to 0. */
  --vs-hero:  "opsz" 144, "SOFT" 0, "WONK" 1;
  --vs-head:  "opsz" 96,  "SOFT" 0, "WONK" 1;
  --vs-mid:   "opsz" 72,  "SOFT" 0, "WONK" 0;
  /* Nothing smaller gets an explicit setting. Below mid size the browser's
     own `font-optical-sizing: auto` already tracks opsz to the font-size,
     which is the correct answer, and WONK is 0 by default. */

  /* Fraunces sets a touch wider than Bodoni at the same tracking, and its
     hairlines need the air, so display tracking opens by 0.004em. */
  /* TRACKING IS A FUNCTION OF SIZE, so a 15% larger scale needs a tighter
     one: the same -0.018em that reads as controlled at 118px reads as
     loose at 136px, because the gaps grew with the letters while the eye's
     tolerance did not. Tightened one step each. This is the difference
     between type that is BIGGER and type that is more IMPACTFUL — the
     first is a number, the second is the setting. */
  --tr-display: -0.024em;   /* tight, but never welded */
  --tr-head:    -0.016em;
  --tr-plate:    0.42em;    /* the wide one. This is the reference's signature. */

  /* ── Rhythm ──────────────────────────────────────────────────────── */
  --gutter:    clamp(20px, 4.6vw, 68px);
  --measure:   1560px;
  /* Was clamp(96px, 15vh, 210px), which spent about 270px of empty ground
     per scene at a 900px viewport and made the page read as a stack of
     separated slides. Tightened: the scenes still breathe, and roughly
     700px of pure gap comes off the document. */
  --scene:     clamp(72px, 10.5vh, 152px);

  /* ── Motion ──────────────────────────────────────────────────────────
     TWO easings and no more. `--ease-ui` is the one every hover, button and
     state change uses, at one of two durations; `--ease` is the slow
     cinematic curve reserved for scenes. Mixing a dozen curves is what makes
     a site feel assembled rather than designed. */
  --ease-ui:   cubic-bezier(0.4, 0, 0.2, 1);
  --dur-ui:    0.4s;
  --dur-fast:  0.2s;

  --ease:    cubic-bezier(0.16, 1, 0.3, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);
  --dur:     0.9s;
}

/* ── Reset ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }
body, h1, h2, h3, h4, p, figure, ol, ul, dl { margin: 0; }
ol, ul { padding: 0; list-style: none; }
img, video, canvas, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }

html { scroll-behavior: auto; }               /* Lenis owns the scroll */
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-sans);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.62;
  letter-spacing: 0.004em;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ── The ground ───────────────────────────────────────────────────────
   The page never sits on one flat ivory. This single fixed layer changes
   colour as the story moves — paper for the editorial scenes, a warmer
   sand where the work is shown, near-dark under the film — so the whole
   site reads as one continuous world that lights differently per scene. */
.ground {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--paper);
  transition: background-color 1.1s var(--ease-io);
}
/* THE GROUND NO LONGER CHANGES COLOUR. It used to shift paper / sand /
   warm / deep per scene, which is precisely the "different background per
   section" the sheet is meant to replace. One tone now, under everything.
   `data-ground` still steps, because the film needs its ink inverted and
   there is no half-inverted; only the colour has stopped moving. */
.ground { background-color: var(--paper); }

/* Ink has to invert over the dark ground or the scene goes unreadable.
   The ground colour itself is interpolated in JS and never steps, but this
   one cannot be half-applied, so it is a switch — given a transition long
   enough that it lands inside the ground's own fade rather than on top of
   it. Without it there is a single frame where the page changes colour. */
body { transition: color 0.9s var(--ease-io); }
body[data-ground="deep"] { color: #f8f6f1; }
body[data-ground="deep"] .plate {
  color: rgba(248,246,241,0.82);
  --plate-ink: rgba(248,246,241,0.82);
  --plate-sweep: rgba(255,255,255,0.95);
}

/* ── Surfaces ─────────────────────────────────────────────────────────
   The panel every visual sits in. A hairline, a radius, and a faint inner
   highlight along the top edge so it reads as a lit surface rather than a
   cropped rectangle. */
.surface {
  position: relative;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  overflow: hidden;
  background: var(--paper-2);
}
.surface::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
}
.surface--sm { border-radius: var(--r-md); }

/* ── Glow ─────────────────────────────────────────────────────────────
   Depth from light rather than from objects. One soft radial behind a
   scene does what a WebGL layer would, at no library cost. */
.glow {
  position: absolute; pointer-events: none; z-index: 0;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.5;
}
.glow--warm { background: radial-gradient(circle, rgba(216,209,197,0.9), rgba(216,209,197,0) 70%); }
.glow--red  { background: radial-gradient(circle, rgba(168,50,36,0.16), rgba(168,50,36,0) 70%); }

/* ═══ AMBIENT ══════════════════════════════════════════════════════════
   Nothing on this site is allowed to sit completely dead. Four cheap,
   GPU-only loops give every screen a pulse without ever becoming noise.
   All four are CSS keyframes on composited properties, so the cost is a
   handful of layers rather than a render loop.

   Every one of them is switched off under prefers-reduced-motion. */

/* 1. The light breathes and drifts. */
@keyframes sys-breathe {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1);      opacity: 0.42; }
  50%      { transform: translate3d(2.5%, -2%, 0) scale(1.09); opacity: 0.62; }
}

/* 2. A slow sweep across the small tracked labels, so the quietest type on
      the page is still alive. */
@keyframes sys-shimmer {
  0%   { background-position: 240% 0, 0 0; }
  100% { background-position: -140% 0, 0 0; }
}

/* 3. A hairline that travels along an edge, the way a lit surface catches
      the light as you move past it. */
@keyframes sys-sheen {
  0%   { transform: translateX(-130%); }
  100% { transform: translateX(230%); }
}

/* 4. The grain itself moves, a pixel at a time. Without this the tooth on
      the paper is the one part of the page that is provably frozen. */
@keyframes sys-grain {
  0%, 100% { transform: translate3d(0, 0, 0); }
  25%      { transform: translate3d(-1.5%, 1%, 0); }
  50%      { transform: translate3d(1%, -1.5%, 0); }
  75%      { transform: translate3d(1.5%, 1.5%, 0); }
}

.glow { animation: sys-breathe 17s var(--ease-io) infinite; }
.glow--slow { animation-duration: 26s; animation-direction: alternate; }

/* The plate label carries the sweep. `background-clip:text` needs the
   fallback guarded, or an engine without it paints transparent text. */
/* The guard covers BOTH features this rule depends on. If either is
   unsupported the whole `background-image` value is dropped, and since the
   text fill is transparent the label would render as nothing at all. That
   is not hypothetical: an unguarded color-mix() inside the gradient made
   every plate on the site invisible. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .plate > span {
    /* TWO layers, and the order matters. The lower layer is solid
       currentColor so every glyph is always painted: with a single moving
       gradient, any part of the text the gradient had not reached rendered
       as nothing, and a long label lost its first words entirely.
       The upper layer is the sweep, and it DARKENS rather than lightens,
       so every frame sits at or above the label's resting contrast. */
    /* NOT currentColor. This element's own `color` is transparent so the
       glyphs can show the background through them, which means currentColor
       here resolves to transparent and the base layer paints nothing: the
       label disappears completely. The tone comes from --plate-ink instead,
       and the fallback means a context that forgets to set it degrades to a
       visible label rather than an invisible one. */
    background-image:
      linear-gradient(100deg, transparent 40%, var(--plate-sweep, rgba(23,20,18,0.85)) 50%, transparent 60%),
      linear-gradient(var(--plate-ink, #6e6a61), var(--plate-ink, #6e6a61));
    background-size: 220% 100%, 100% 100%;
    background-repeat: no-repeat, no-repeat;
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
    animation: sys-shimmer 9s linear infinite;
  }
}

/* A travelling sheen for surfaces that want a lit edge. */
.sheen { position: relative; overflow: hidden; }
.sheen::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: linear-gradient(100deg, transparent 38%,
              rgba(255,255,255,0.32) 50%, transparent 62%);
  animation: sys-sheen 9s var(--ease-io) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .glow, .plate > span, .sheen::before { animation: none !important; }
  .plate > span { -webkit-text-fill-color: currentColor; color: currentColor;
                  background: none; }
  body::after { animation: none !important; }
}

/* ── Paper grain ──────────────────────────────────────────────────────
   The references are all photographs of printed paper. One fixed, tiny,
   tiled SVG turbulence gives the ground a tooth without shipping an image
   or costing a repaint. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  opacity: 0.42;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.34'/%3E%3C/svg%3E");
  /* Oversized so the drift never exposes an edge. */
  inset: -3%;
  animation: sys-grain 8s steps(4, end) infinite;
}

/* ═══ TYPE ══════════════════════════════════════════════════════════════ */

/* `opsz` is what makes a variable editorial serif work at display size:
   the high-optical-size master is where the thick/thin contrast and the
   hairline serifs live, and it is the whole reason this type can carry a
   hero at weight 420. `font-optical-sizing: auto` is kept as the fallback
   for anything that does not set the axis explicitly. */
.display, .h-display, h1, h2 {
  font-family: var(--f-display);
  font-optical-sizing: auto;
  font-variation-settings: var(--vs-head);
  font-weight: var(--w-display);
  letter-spacing: var(--tr-display);
  line-height: 1.04;
  text-wrap: balance;
}
h3, h4 { font-family: var(--f-display); font-weight: var(--w-head);
         font-variation-settings: var(--vs-mid); letter-spacing: var(--tr-head); }

/* Line-height opens slightly against the old Bodoni values: Fraunces has a
   taller ascender and a deeper descender, and at 0.96 the hero's second
   line sat on the first line's descenders. */
.t-hero    { font-size: var(--t-hero);    line-height: 0.99;
             font-variation-settings: var(--vs-hero); }
.t-display { font-size: var(--t-display); line-height: 1.02;
             font-variation-settings: var(--vs-hero); }
.t-head    { font-size: var(--t-head);    line-height: 1.08; letter-spacing: var(--tr-head);
             font-weight: var(--w-head); font-variation-settings: var(--vs-head); }

/* Small text must NOT inherit the display optical size. */
p, li, label, input, select, textarea, button, .plate, .meta {
  font-family: var(--f-sans);
  font-variation-settings: normal;
}

/* ── The plate ────────────────────────────────────────────────────────
   Reference 2's "WEDDING —— EVENTS": tiny, tracked to the edge of legibility,
   with hairlines running out of it. Every section on this site is announced
   by one of these and by nothing else. */
.plate {
  display: flex;
  align-items: center;
  gap: 0.95rem;
  font-size: var(--t-plate);
  font-weight: 500;
  letter-spacing: var(--tr-plate);
  text-transform: uppercase;
  color: var(--ink-3);
  --plate-ink: var(--ink-3);
  line-height: 1;
}
.plate > span { white-space: nowrap; text-indent: var(--tr-plate); }

/* ── The brace, and the folio ─────────────────────────────────────────
   Two devices taken from the moodboards, and the first one is the reason
   the pair belongs to THIS brand rather than to a mood: the boards set
   their small labels inside decorative braces — { City tours } — and a
   brace is also the single most recognisable mark in the language this
   studio actually writes in. It is an editorial ornament and a piece of
   code at the same time, which is precisely what a publication about
   building websites should be setting its chapter heads in.

   The folio numeral is the second: the boards number their spreads, and
   numbering is what makes a set of sections read as chapters of one
   object instead of as blocks on a page. Homepage chapters only, in
   document order, and never on an inner page — a folio that restarts on
   every page is a decoration, not a system.

   Both are set in --ink-soft against the label's own --ink-3, so the
   ornament is quieter than the words it frames. */
.plate > span::before,
.plate > span::after {
  color: var(--ink-soft);
  font-weight: 400;
  letter-spacing: 0;
}
.plate > span::before { content: "{"; margin-right: 0.8em; }
/* The label's own tracking already leaves about a third of an em after
   its last letter, so the closing brace needs LESS margin than the
   opening one or the pair sits visibly off-centre around the words. */
.plate > span::after  { content: "}"; margin-left: 0.42em; }
/* attr() resolves against the element the pseudo belongs to, so the
   numeral lives on the SPAN, not on the plate. */
/* THE NUMERAL IS INFORMATION, THE BRACE IS AN ORNAMENT, and they cannot
   share --ink-soft: at 3.9:1 that token is decorative-only by measurement,
   and a chapter number is small text that has to be readable. The folio
   pseudo therefore takes the label's own ink. */
.plate > span[data-folio]::before {
  color: var(--ink-3);
  content: "{ " attr(data-folio);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
}
.plate::before, .plate::after {
  content: "";
  height: 1px;
  background: currentColor;
  opacity: 0.5;
  flex: 0 0 clamp(1.6rem, 3.4vw, 3.2rem);
}
/* Left-aligned variant — rule only on the trailing side. */
.plate--start::before { display: none; }
.plate--start { justify-content: flex-start; }
.plate--start::after { flex: 1 1 auto; max-width: 8rem; }
.plate--center { justify-content: center; }

/* ── The mark, stacked ────────────────────────────────────────────────
   Line one: "site" in black editorial serif, then a RED full stop.
   Line two: "Your Story" directly underneath, in the script, in red.
   The script is optically larger than its line box, so the two lines are
   pulled together with a negative margin rather than a smaller size. */
.mark {
  display: inline-block;
  /* The wordmark, and only the wordmark, stays on the Didone it was
     approved in. It is loaded as a single 600 instance for this purpose. */
  font-family: var(--f-mark);
  font-variation-settings: "opsz" 48;
  font-weight: 600;
  letter-spacing: -0.012em;
  line-height: 1;
  color: var(--ink);
  white-space: nowrap;
}
.mark-site  { display: block; color: var(--ink); width: max-content; }

/* "SITE" SITS OVER THE GAP, NOT OVER THE Y.
   Where that gap actually falls depends on the script face, the size, the
   letter-spacing and the screen — so it is measured (motion/ui.js writes
   --mark-gap, the distance from the lockup's left edge to the centre of
   the space between "Your" and "Story") rather than guessed at one
   breakpoint. The word is then centred on that point.

   The positioning only applies once the measurement exists, so a page
   whose JavaScript has not run yet shows the lockup it always showed
   instead of a word yanked half its width to the left. */
.mark[data-lockup="1"] .mark-site {
  position: relative;
  left: var(--mark-gap, 0px);
  transform: translateX(-50%);
}
.mark-dot   { color: var(--red); }
/* SITE SITS DIRECTLY ABOVE YOUR STORY, WITH A GAP THAT IS DELIBERATE.
   The old -0.14em was closing the space between the two lines because
   Pinyon's ascenders filled it; Italianno's are finer and taller, so the
   lockup needs the room given back rather than taken away. The ratio comes
   down 1.72 -> 1.5em as well: Italianno sets wider on the em than Pinyon,
   so the signature keeps its presence at a smaller number, and a smaller
   number is thinner strokes. */
.mark-story {
  display: block;
  font-family: var(--f-script);
  font-weight: 400;
  color: var(--red);
  /* A hairline hand sets far smaller on the em than a pen one, so the
     ratio goes UP while the strokes get thinner — the two are independent,
     which is the whole reason this face can be larger and lighter at once. */
  font-size: 1.85em;
  line-height: 0.78;
  letter-spacing: 0;
  /* Measured in INK, not in boxes: the distance from the lowest pixel of
     "site." to the top of the signature's body, at 0.18em, is about half
     the serif's own cap height. Close enough to read as one lockup, clear
     enough that nothing touches. */
  margin-top: 0.18em;
  margin-left: 0.04em;
}

/* ── The script ───────────────────────────────────────────────────────
   Reference 1 and 2 both cross a script word over a set word rather than
   placing it beside one. `.script-cross` is that move: it sits on its own
   line box but is pulled up and rotated so it overlaps what came before. */
.script {
  font-family: var(--f-script);
  font-weight: 400;
  color: var(--red);
  letter-spacing: 0;
  line-height: 0.8;
  font-size: 1.72em;      /* the hairline hand runs small on the em */
  display: inline-block;
  vertical-align: baseline;
}
.script-cross {
  position: relative;
  display: inline-block;
  margin-left: -0.14em;
  transform: translateY(-0.05em) rotate(-3deg);
  transform-origin: 22% 60%;
}

/* ── Body ─────────────────────────────────────────────────────────── */
.lede { font-size: var(--t-lede); line-height: 1.62; color: var(--ink-2); max-width: 46ch; }
.sub  { font-size: var(--t-sub);  line-height: 1.46; color: var(--ink-2); max-width: 30ch;
        font-family: var(--f-display); font-weight: 450; font-style: italic;
        font-variation-settings: var(--vs-mid); letter-spacing: -0.004em; }
.meta { font-size: 0.9rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--ink-3); }
.num  { font-family: var(--f-sans); font-weight: 300; font-variant-numeric: tabular-nums; }

/* ── Shell ────────────────────────────────────────────────────────── */
.wrap  { width: 100%; max-width: var(--measure); margin-inline: auto; padding-inline: var(--gutter); }
.scene { padding-block: var(--scene); position: relative; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* HIDDEN BY ITS OWN HEIGHT, NOT BY A GUESSED OFFSET. `top: -60px` on a
   box that was 52px tall worked until the type scale grew 15% and the box
   went to 72px: twelve pixels of a black bar then sat over the masthead on
   every phone. Translating by its own height cannot drift, whatever the
   font size does. */
.skip {
  position: absolute; left: 50%; top: 12px;
  transform: translate(-50%, calc(-100% - 24px));
  z-index: 200; background: var(--ink); color: var(--paper);
  padding: 12px 22px; font-size: 0.92rem; letter-spacing: 0.14em; text-transform: uppercase;
  transition: transform 0.2s var(--ease-ui);
}
.skip:focus-visible { transform: translate(-50%, 0); }

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 3px;
}

/* ── iOS, and the two things it does differently ──────────────────────
   1. THE ADDRESS BAR CHANGES THE VIEWPORT HEIGHT AS YOU SCROLL. Every
      full-height box on this site is already `svh` — the SMALL viewport
      height, the size with the bar showing — and that is deliberate in
      preference to `dvh`: `dvh` is the CURRENT height, so it changes while
      the bar retracts and everything sized in it resizes mid-scroll, which
      is the layout jump the brief describes. `svh` is a fixed number for
      the life of the page. The cost is a strip of extra room at the bottom
      once the bar hides, which is invisible on a dark stage and preferable
      to a moving layout.
   2. THE SAFE AREA. With `viewport-fit=cover` in the meta (added for this),
      `env(safe-area-inset-*)` finally reports real numbers, so the page
      gutter carries the left and right inset on a notched phone and the
      film's own copy carries the bottom one, clear of the home indicator. */
:root {
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}
@supports (padding: env(safe-area-inset-left)) {
  @media (max-width: 860px) {
    .wrap { padding-left: max(var(--gutter), var(--safe-l));
            padding-right: max(var(--gutter), var(--safe-r)); }
  }
}

/* The only scroll lock on the site, and it exists for the menu overlay
   alone. Nothing locks the page during the film. */
html[data-locked="1"], html[data-locked="1"] body {
  overflow: hidden;
  overscroll-behavior: none;
}

/* ── Rules ────────────────────────────────────────────────────────── */
.rule { height: 1px; background: var(--line); border: 0; }

/* ═══ THE PAPER ═══════════════════════════════════════════════════════
   ONE SHEET, FOR THE WHOLE SITE.

   What replaced the glass atmosphere. The references are three pieces of
   old paper: a folded letter with faint script and a postmark, a bleached
   antique map, and a torn sheet lying on a bed of collage scraps. Three
   things carry across from them, and none of them is the subject matter.

   1. The tone is warm off-white with tonal DRIFT, never a flat fill. Real
      paper is lighter where it was pressed and darker where it was
      handled, so the base carries a few very large, very soft gradients
      that no eye reads as gradients.
   2. The line work is extremely fine and extremely pale. On the map the
      coastlines and graticules are barely darker than the sheet; that is
      what makes it read as printed rather than drawn on.
   3. The third reference is the composition rule: the torn sheet is CLEAN
      IN THE MIDDLE and every scrap, star and torn edge sits around the
      outside. Content lives in the clean middle. Artifacts live at the
      edges. Everything below follows from that.

   The layer is fixed, so it never restarts, never resets at a section
   boundary, and has no rectangle of its own. Sections stay transparent;
   this is what a visitor is actually looking at from the first pixel of
   the hero to the last of the footer. */
.paper {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--paper);
}
/* THE FLOW (motion/flow.js). First child of the sheet, so it paints over
   the flat paper colour and UNDER fibre, grain, crush, craze, creases,
   wear, stamp and edge — every one of which comes after it in the DOM.
   That ordering is the integration: the tone drifts inside the paper
   instead of on top of it, and the sheet stays the thing you see.

   No blend mode on purpose. The colours it draws ARE the paper's own, so
   normal compositing at part opacity is enough, and mix-blend-mode is one
   of the few things measured to cost real frames on a phone here. */
.paper-flow {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%;
  pointer-events: none;
  opacity: 0.62;
}
@media (pointer: coarse) { .paper-flow { opacity: 0.42; } }
/* Reduce motion keeps the tone and loses the movement, so it can afford
   to sit a little further back. */
.paper-flow[data-still="1"] { opacity: 0.5; }

.paper > span { position: absolute; inset: 0; display: block; }
.paper-crush { position: absolute; inset: 0; }

/* 1 · TONAL DRIFT. Six very large, very soft pools, none of them centred,
   so no edge of any one of them can be found. Two more than before and
   deeper, because a flat fill is the single thing that gives a CSS
   background away: real stock is lighter where it was pressed and darker
   where it was handled, at a scale much larger than the viewport. */
.paper-fibre {
  background:
    radial-gradient(58% 44% at 12% 8%,  rgba(255,253,246,0.80), rgba(255,253,246,0) 70%),
    radial-gradient(52% 40% at 88% 22%, rgba(223,214,197,0.58), rgba(223,214,197,0) 72%),
    radial-gradient(64% 50% at 74% 88%, rgba(255,253,246,0.62), rgba(255,253,246,0) 70%),
    radial-gradient(48% 38% at 22% 76%, rgba(219,209,190,0.50), rgba(219,209,190,0) 74%),
    radial-gradient(40% 34% at 52% 40%, rgba(255,254,250,0.42), rgba(255,254,250,0) 76%),
    radial-gradient(44% 30% at 4% 52%,  rgba(214,203,183,0.34), rgba(214,203,183,0) 78%);
}

/* 2 · THE TOOTH. Fractal noise at THREE scales now: a very fine one for
   the fibre, a mid one for the mottling, and a coarse one for the cloud
   of tone that keeps the other two from reading as a filter. Inline SVG
   rather than rasters, so the whole surface costs about two kilobytes and
   never blurs at any DPR. Raised 0.055 -> 0.085: the sheet is meant to be
   felt from a foot away, not only found by pressing your face to it. */
.paper-grain {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='pg1'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23pg1)' opacity='0.55'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='pg2'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23pg2)' opacity='0.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='520' height='520'%3E%3Cfilter id='pg3'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.028' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='520' height='520' filter='url(%23pg3)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 180px 180px, 220px 220px, 520px 520px;
  mix-blend-mode: multiply;
  opacity: 0.085;
}

/* 3 · CRAZING, and it is the layer that reads as AGE rather than as
   texture. The first reference's sheet is covered in a fine web of
   surface cracks, and contours are how you draw that cheaply: turbulence
   gives a cloud, and feComponentTransfer slices that cloud into contour
   lines. TWO THINGS DECIDE WHETHER THIS READS AS PAPER OR AS CAMOUFLAGE:
   the noise must be `turbulence` (veiny) rather than `fractalNoise`
   (blobby), and the alpha table must be a SINGLE narrow step in a long
   run of zeros — a `table` with wide spikes renders soft bands the size of
   a fist, which is exactly what the first attempt did. Two scales, both
   barely present. */
.paper-craze {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='560' height='560'%3E%3Cfilter id='pk1'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.055' numOctaves='4' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='560' height='560' filter='url(%23pk1)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='340' height='340'%3E%3Cfilter id='pk2'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.13' numOctaves='3' seed='19' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='340' height='340' filter='url(%23pk2)'/%3E%3C/svg%3E");
  background-size: 560px 560px, 340px 340px;
  mix-blend-mode: multiply;
  opacity: 0.05;
}

/* 3b · CRUSH. What separates a sheet that was STORED from a sheet that
   was HANDLED: not more grain, but light. A crumpled page is a field of
   shallow facets, and a facet is only visible because one side of it
   catches the light and the other does not.

   `feDiffuseLighting` over a turbulence bump is the honest way to draw
   that — the same maths a renderer uses — and it costs nothing here
   because the layer is fixed, painted once, and never animates. A distant
   light (azimuth 300, elevation 62) rakes across the surface from the top
   left, which is where every other highlight on this sheet already comes
   from; `surfaceScale` is deliberately tiny, because the difference
   between paper that has been handled and paper that has been screwed
   into a ball is entirely in that number.

   Soft-light rather than multiply: crush has to be able to lighten as
   well as darken, or the facets read as stains instead of as relief. */
.paper-crush {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='620'%3E%3Cfilter id='pc1'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.009' numOctaves='4' seed='23' stitchTiles='stitch' result='n'/%3E%3CfeDiffuseLighting in='n' surfaceScale='1.6' diffuseConstant='1' lighting-color='%23ffffff'%3E%3CfeDistantLight azimuth='300' elevation='62'/%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3Crect width='620' height='620' filter='url(%23pc1)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='980' height='980'%3E%3Cfilter id='pc2'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.0032' numOctaves='3' seed='11' stitchTiles='stitch' result='n'/%3E%3CfeDiffuseLighting in='n' surfaceScale='2.4' diffuseConstant='1' lighting-color='%23ffffff'%3E%3CfeDistantLight azimuth='300' elevation='58'/%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3Crect width='980' height='980' filter='url(%23pc2)'/%3E%3C/svg%3E");
  background-size: 620px 620px, 980px 980px;
  mix-blend-mode: soft-light;
  opacity: 0.42;
}

/* 3c · THE SLOW CREASES. Four long, very shallow bends running at angles
   the fold grid does not use, so the sheet reads as having been rolled or
   pressed as well as folded. Each is a hairline of shade with its own
   catchlight, at a third the strength of a real crease. */
.paper-crush::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(102deg, transparent 21%, rgba(23,20,18,0.016) 22%,
                    rgba(255,255,255,0.30) 22.6%, transparent 24%),
    linear-gradient(78deg,  transparent 57%, rgba(23,20,18,0.014) 58%,
                    rgba(255,255,255,0.26) 58.5%, transparent 60%),
    linear-gradient(168deg, transparent 34%, rgba(23,20,18,0.012) 35%,
                    rgba(255,255,255,0.24) 35.5%, transparent 37%),
    linear-gradient(186deg, transparent 72%, rgba(23,20,18,0.014) 73%,
                    rgba(255,255,255,0.26) 73.5%, transparent 75%);
}

/* 4 · THE FOLDS. The letter in the first reference was creased into six,
   and the creases are what make it read as a sheet that was once inside
   an envelope. Two verticals and two horizontals, each a hairline of
   shadow with a hairline of catchlight beside it, plus the soft swell
   either side — because a crease from a distance is not a line, it is a
   bend in the surface. */
.paper-creases {
  background:
    linear-gradient(90deg, transparent calc(33.4% - 1.5px), rgba(23,20,18,0.045) 33.4%,
                    rgba(255,255,255,0.55) calc(33.4% + 1px), transparent calc(33.4% + 2.5px)),
    linear-gradient(90deg, transparent calc(71% - 1.5px), rgba(23,20,18,0.036) 71%,
                    rgba(255,255,255,0.5) calc(71% + 1px), transparent calc(71% + 2.5px)),
    linear-gradient(0deg,  transparent calc(46% - 1.5px), rgba(23,20,18,0.038) 46%,
                    rgba(255,255,255,0.5) calc(46% + 1px), transparent calc(46% + 2.5px)),
    linear-gradient(0deg,  transparent calc(78% - 1.5px), rgba(23,20,18,0.028) 78%,
                    rgba(255,255,255,0.42) calc(78% + 1px), transparent calc(78% + 2.5px)),
    linear-gradient(90deg, rgba(23,20,18,0) 26%, rgba(23,20,18,0.016) 33.4%, rgba(23,20,18,0) 41%),
    linear-gradient(0deg,  rgba(23,20,18,0) 38%, rgba(23,20,18,0.014) 46%,   rgba(23,20,18,0) 54%);
}

/* 5 · WEAR. Foxing and print wear: a handful of very faint warm stains,
   none of them circular enough to be found, plus three dry patches where
   the surface has been rubbed lighter. All of them sit AWAY from the
   middle of the frame, which is the third reference's whole lesson — the
   sheet is clean exactly where you would put something on it. */
.paper-wear {
  background:
    radial-gradient(22% 16% at 6% 18%,  rgba(150,124,86,0.055), rgba(150,124,86,0) 68%),
    radial-gradient(16% 13% at 95% 62%, rgba(150,124,86,0.05),  rgba(150,124,86,0) 66%),
    radial-gradient(13% 20% at 88% 6%,  rgba(139,116,82,0.04),  rgba(139,116,82,0) 70%),
    radial-gradient(19% 12% at 3% 88%,  rgba(139,116,82,0.045), rgba(139,116,82,0) 68%),
    radial-gradient(9%  8%  at 74% 95%, rgba(150,124,86,0.05),  rgba(150,124,86,0) 64%),
    radial-gradient(11% 9%  at 22% 3%,  rgba(255,255,252,0.5),  rgba(255,255,252,0) 70%),
    radial-gradient(14% 10% at 64% 97%, rgba(255,255,252,0.42), rgba(255,255,252,0) 72%);
}

/* 6 · THE POSTMARK. Rings cropped by the corners exactly as the
   reference's is, at the opacity of something stamped eighty years ago
   through a sheet that has been washed since. They are the only circular
   things on the page, so they are found rather than noticed. */
.paper-stamp {
  background:
    radial-gradient(circle at -2% 92%, transparent 118px, rgba(94,78,58,0.06) 119px,
                    rgba(94,78,58,0.06) 121px, transparent 122px),
    radial-gradient(circle at -2% 92%, transparent 150px, rgba(94,78,58,0.045) 151px,
                    rgba(94,78,58,0.045) 152.5px, transparent 154px),
    radial-gradient(circle at 104% 12%, transparent 96px, rgba(94,78,58,0.04) 97px,
                    rgba(94,78,58,0.04) 98.5px, transparent 100px);
}

/* 7 · THE HANDLED EDGE. The sheet is darker where it has been held, and
   this is also what stops the paper reading as a screen-sized rectangle:
   there is no visible border, only a fall-off. */
.paper-edge {
  background: radial-gradient(124% 110% at 50% 46%,
    rgba(23,20,18,0) 54%, rgba(23,20,18,0.03) 78%, rgba(23,20,18,0.062) 100%);
}

/* The reel arrows measured 32x31: fine under a cursor, too small under a
   thumb. They grow to the 44px minimum ON COARSE POINTERS ONLY, so the
   desktop composition is untouched and the tablet one is usable. */
@media (pointer: coarse) {
  .voices-btn { min-width: 44px; min-height: 44px; }
}
.voices-btn {
  background: none; border: 1px solid var(--line); border-radius: var(--r-full);
  width: 46px; height: 46px; cursor: pointer; color: var(--ink);
  transition: background var(--dur-ui) var(--ease-ui), color var(--dur-ui) var(--ease-ui), border-color var(--dur-ui) var(--ease-ui);
}
.voices-btn:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* ═══ THE MARKS ════════════════════════════════════════════════════════
   The artifacts printed on the sheet: browser outlines, wireframes,
   cursors, brackets, sitemaps, grids, code fragments, registration marks
   and the four-point stars from the collage reference.

   This layer is ABSOLUTE, not fixed, and as tall as the document, so the
   marks travel with the page and are discovered rather than presented.
   The paper underneath stays fixed, which is what keeps the sheet
   continuous while its printing scrolls past.

   Every mark is placed by hand in motion/paper.js at a percentage down
   the page, weighted to the left and right margins. Nothing is centred,
   nothing repeats on a grid, and none of them is legible: they are the
   memory of a website being drawn, not a diagram of one. */
.marks {
  position: absolute; top: 0; left: 0; width: 100%;
  z-index: -1; pointer-events: none; overflow: clip;
}
.mark-i {
  position: absolute; display: block;
  color: var(--ink);
  will-change: transform;
}
/* The rotation each mark carries is written to `transform` inline, and the
   parallax writes yPercent to the same property — GSAP composes the two
   because it parses the existing transform first, which is exactly why the
   rotation must stay an inline style and never move into a class. */
.mark-i svg { display: block; width: 100%; height: auto; overflow: visible; }
/* Everything is drawn, nothing is filled, and the stroke is a hairline at
   every scale because it is held at SCREEN scale rather than scaled with
   the SVG box: a 520px arc set and a 20px star draw the same weight of
   line, which is what makes seventy marks read as one printing. */
.mark-i svg [stroke] { vector-effect: non-scaling-stroke; }

/* The map-scale work — the rhumb roses and the graticule arcs — is the
   only printing allowed across the middle of the page, and it earns that
   by being drawn at half the weight of everything else and masked away
   from the centre of its own box. Same device as the bleached reference:
   covered edge to edge, and still reads as blank paper from a distance. */
.mark-i--map svg [stroke] { stroke-opacity: 0.62; }
.mark-i--map {
  -webkit-mask-image: radial-gradient(58% 58% at 50% 50%, transparent 0%,
                      rgba(0,0,0,0.55) 62%, #000 100%);
          mask-image: radial-gradient(58% 58% at 50% 50%, transparent 0%,
                      rgba(0,0,0,0.55) 62%, #000 100%);
}

@media (prefers-reduced-motion: reduce) {
  .mark-i { transform: none !important; }
}

/* ═══ DEPTH ════════════════════════════════════════════════════════════
   A depth scale, not a pile of objects. `data-depth` runs 1 (furthest,
   barely moves) to 24 (nearest, moves most), and one handler converts it
   to a parallax rate. The point of a 24-step scale is that layers can be
   separated FINELY: the atmosphere at 2, 3 and 4 is only just apart, and
   that is exactly why the three plates read as one space with air in it
   rather than three pictures. Nothing is obliged to use every step, and
   most scenes use three or four. */
:root { --depth-unit: 0.55px; }



/* ═══ LUME — a ring of moving light ════════════════════════════════════
   The reference's signature "alive" device, and the single most repeated
   thing on it: nine instances of a masked border with a gradient spinning
   behind it, running at once. Reading its DOM, the trick is a wrapper
   with `overflow: hidden`, an inner element that is `absolute inset-0`
   with a mask compositing `exclude` so only the border ring survives, and
   a child spinning `rotate(1turn)` on a 4s linear loop.

   Reproduced here with one pseudo-element instead of three, by animating
   the conic gradient's own angle through a registered custom property.
   Registering it is what makes it interpolate at all: an unregistered
   custom property is a string to the animation engine and snaps. This
   also avoids the distortion their version has to work around, because
   nothing is being rotated, only a colour stop travelling around a ring.

   The ring is red, at low opacity, and slow. On this palette it reads as
   light catching an edge rather than as a glowing border, which is the
   difference between editorial and gaming. */
@property --lume-a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.lume { position: relative; isolation: isolate; }
.lume::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  border-radius: inherit; padding: 1px;
  background: conic-gradient(from var(--lume-a),
      transparent 0deg, transparent 24deg,
      var(--red) 52deg, rgba(168,50,36,0.25) 78deg,
      transparent 118deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000, #000) content-box,
                linear-gradient(#000, #000);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000, #000) content-box,
                linear-gradient(#000, #000);
          mask-composite: exclude;
  opacity: 0.55;
  pointer-events: none;
  animation: sys-lume 7.5s linear infinite;
}
.lume--slow::before { animation-duration: 12s; opacity: 0.4; }
@keyframes sys-lume { to { --lume-a: 360deg; } }

/* A dark panel needs the ring brighter to read at all. */
.lume--onDark::before { opacity: 0.85; }

@media (prefers-reduced-motion: reduce) {
  .lume::before { animation: none; opacity: 0.3; }
}
