/* =========================================================================
   Data Center in My Backyard
   =========================================================================
   Layout model
   ------------
   Everything lives inside `.stage`, a box locked to the exact aspect ratio of
   the animation (1618 x 1086). Because the stage matches the artwork, every
   control can be positioned in PERCENTAGES of the artwork and will stay glued
   to the right spot at any window size.

   The data center's bounding box inside the frame is stored in the --dc-*
   variables below. Tune those and the arrows, slider and effects all follow.
   ========================================================================= */

:root {
  /* Source frame dimensions — must match the mp4 / homepage still.
     16:9, so on a typical widescreen browser the artwork fills the window
     edge to edge with no letterbox fill showing at all. */
  --frame-w: 16;
  --frame-h: 9;

  /* Data center bounding box on the *homepage plate*, as % of the frame.
     Measured off the final frame. The homepage effect layers hang off this;
     views with their own scene position their own artwork instead. */
  --dc-left:   29.6%;
  --dc-right:  69.5%;
  --dc-top:    36.3%;
  --dc-bottom: 77.9%;
  --dc-cx:     49.5%;   /* horizontal centre of the building */
  --dc-cy:     57.1%;   /* vertical centre of the building   */

  /* How far in from the stage's edges the arrows sit. Not tied to the
     artwork, so it survives a view moving the building. */
  --arrow-inset: 4.6%;

  /* Chrome */
  --ink:        #1e293b;
  --ink-soft:   #64748b;
  --glass:      rgba(255, 255, 255, 0.62);
  --glass-line: rgba(255, 255, 255, 0.85);
  --shadow:     0 2px 10px rgba(30, 41, 59, 0.10), 0 8px 30px rgba(30, 41, 59, 0.08);

  /* ── Panel glass ─────────────────────────────────────────────────────────
     The paragraph boxes, and only those: the arrows still take --glass above,
     because they are 44px circles and a 26px blur inside one is just fog.

     The fill is a top-to-bottom gradient rather than a flat wash, and the
     border is a second gradient clipped to the border box — bright along the
     top edge, cool along the bottom. That pair is what reads as a lit sheet of
     glass rather than a translucent rectangle, and it is why the border has to
     be `transparent`: the colour comes from the background's border-box layer,
     not from the border itself. */
  --panel-fill:   rgba(255, 255, 255, 0.42);
  --panel-fill-2: rgba(255, 255, 255, 0.21);
  --panel-edge:   linear-gradient(160deg,
                    rgba(255, 255, 255, 1),
                    rgba(255, 255, 255, 0.25) 42%,
                    rgba(148, 163, 184, 0.5));
  --panel-filter: blur(26px) saturate(2.1) brightness(1.05);
  --panel-radius: 20px;
  --panel-shadow: 0 18px 44px rgba(30, 41, 59, 0.16),
                  0 3px 8px rgba(30, 41, 59, 0.07),
                  inset 0 -1px 2px rgba(255, 255, 255, 0.6);

  /* Set per view from JS */
  --accent: #64748b;
  --fx-strength: 0.4;   /* driven by the slider, 0 → 1 */

  --fade: 550ms;
}

/* Body copy. Recoleta has no italic, so any <em> is a synthesised
   oblique; the weights below are the real cuts. Thin and Light are not shipped because nothing uses them. */

@font-face {
  font-family: "Recoleta";
  src: url("../fonts/recoleta-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("../fonts/recoleta-medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("../fonts/recoleta-semibold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("../fonts/recoleta-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Recoleta";
  src: url("../fonts/recoleta-black.woff2") format("woff2");
  font-weight: 800 900;
  font-style: normal;
  font-display: swap;
}

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

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: #dfeaf2;
  font-family: "Recoleta", Georgia, "Times New Roman", serif;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── Blurred fill behind the letterboxed stage ───────────────────────────────
   A 96px-wide blur of the final frame, stretched to the window. Because it's
   the same picture, the colour at any given x/y roughly matches the artwork
   beside it, so the edge of the stage stops reading as a hard letterbox bar. */
.backdrop {
  position: fixed;
  inset: -5%;
  background: url("../media/backdrop.jpg") center / 100% 100% no-repeat;
  filter: blur(30px) saturate(1.05);
  z-index: 0;
}

/* The animation opens on the forest, so it gets its own fill for the first
   beat. It dissolves on the same curve the video does, landing on the pastel
   fill underneath by the time the title card appears. */
.backdrop--intro {
  position: fixed;
  inset: -5%;
  background: url("../media/backdrop-intro.jpg") center / 100% 100% no-repeat;
  filter: blur(30px) saturate(1.05);
  z-index: 0;
  opacity: 0;
}
:root[data-phase="intro"] .backdrop--intro { opacity: 1; }
:root[data-phase="intro"] .backdrop--intro.is-dissolving {
  animation: intro-fill-out 1.35s ease forwards;
}
@keyframes intro-fill-out {
  0%, 33% { opacity: 1; }
  100%    { opacity: 0; }
}

/* Carries each view's flat wash out over the letterbox fill, so a view like
   "night" darkens the whole window rather than a rectangle in the middle.
   Sits above .backdrop but below the stage, so it never touches the artwork
   or the HUD — the matching .fx layer handles those. */
.backdrop-tint {
  position: fixed;
  inset: -5%;
  z-index: 0;
  opacity: 0;
  transition: opacity 450ms ease;
  pointer-events: none;
}
:root[data-view="sound"]     .backdrop-tint,
:root[data-view="light"]     .backdrop-tint,
:root[data-view="heat"]      .backdrop-tint,
:root[data-view="ecosystem"] .backdrop-tint { opacity: var(--fx-strength); }

:root[data-view="sound"]  .backdrop-tint { background: linear-gradient(180deg, rgba(139,92,246,.06), rgba(124,58,237,.14)); mix-blend-mode: multiply; }
:root[data-view="light"]  .backdrop-tint { background: linear-gradient(180deg, rgba(15,23,42,.86), rgba(30,41,59,.72)); }
:root[data-view="heat"]   .backdrop-tint { background: radial-gradient(100% 80% at 50% 62%, rgba(251,191,36,.18), transparent 78%); mix-blend-mode: multiply; }
:root[data-view="ecosystem"] .backdrop-tint { background: linear-gradient(180deg, rgba(120,113,108,.10), rgba(87,83,78,.20)); mix-blend-mode: multiply; }

/* Must come last: the tint stays out of the way until the intro hands off,
   which matters if you deep-link with ?view=light&intro=1 */
:root[data-phase="intro"] .backdrop-tint { opacity: 0; }

/* ── Layers ──────────────────────────────────────────────────────────────── */
.layer {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  grid-template-areas: "cell";
  z-index: 1;
}
/* Stage and HUD share one grid cell, so they land exactly on top of each other
   without either needing to know the other's size. */
.layer > * { grid-area: cell; }

.layer--home {
  opacity: 0;
  transition: opacity var(--fade) ease;
  z-index: 2;
}
.layer--home.is-visible { opacity: 1; }

.layer--intro {
  transition: opacity 200ms ease;
}
.layer--intro.is-gone {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}

/* ── The stage: aspect-locked, fitted to the viewport ────────────────────── */
.stage,
.hud-layer {
  position: relative;
  aspect-ratio: var(--frame-w) / var(--frame-h);
  width: min(100vw, calc(100dvh * var(--frame-w) / var(--frame-h)));
  max-width: 100vw;
  max-height: 100dvh;
}
.stage { overflow: hidden; }

.hud-layer {
  z-index: 3;
  pointer-events: none;   /* only the controls themselves are clickable */

  /* Lets all the chrome scale in `cqw` units, i.e. proportional to the
     artwork rather than to the browser window. */
  container-type: inline-size;
  container-name: stage;
}
.hud-layer > * { pointer-events: auto; }

.stage-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* A view with its own scene replaces the plate rather than sitting on it, so
   the plate goes away while that scene is up. Belt and braces: the scene paints
   its own background over the whole stage anyway, but if that background is
   slow, missing, or a view is ever added without one, the homepage artwork
   showing through underneath is a confusing failure — two data centers, two
   titles. This makes that impossible. */
.layer--home .stage-media { transition: opacity 420ms ease; }
:root[data-scene] .layer--home .stage-media { opacity: 0; }


/* =========================================================================
   HUD
   ========================================================================= */

.hud {
  position: absolute;
  z-index: 4;
  /* Two layers: the fill clipped to the padding box, the edge to the border
     box. The border itself is transparent and only reserves the 1.4px the
     second layer shows through. */
  background:
    linear-gradient(180deg, var(--panel-fill), var(--panel-fill-2)) padding-box,
    var(--panel-edge) border-box;
  border: 1.4px solid transparent;
  border-radius: var(--panel-radius);
  backdrop-filter: var(--panel-filter);
  -webkit-backdrop-filter: var(--panel-filter);
  box-shadow: var(--panel-shadow);
}

/* ── Homepage note: bottom left ──────────────────────────────────────────── */
/* Clear of all three things it could collide with: the plate paints "Data
   Center" into the top-left and "in my backyard" into the bottom-right, the
   previous-view arrow is vertically centred at `--arrow-inset`, and the slab's
   near corner comes down to about x 470 of 1600. */
.hud--intro {
  left: 5.5%;
  bottom: 6.5%;
  width: clamp(200px, 27cqw, 470px);
  padding: clamp(11px, 1.5cqw, 26px) clamp(13px, 1.75cqw, 30px);
}
.hud--intro[hidden] { display: none; }
.hud--intro p {
  margin: 0;
  font-size: clamp(13px, 1.3cqw, 22px);
  line-height: 1.5;
  color: var(--ink);
}
.hud--intro p + p { margin-top: 0.8em; }
.hud--intro strong { font-weight: 700; }

/* ── Slider: centred, tucked into the gap above the roofline ─────────────── */
.hud--slider {
  left: 50%;
  bottom: calc(100% - var(--dc-top) + 1.4%);
  transform: translateX(-50%);
  width: clamp(240px, 40cqw, 520px);

  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(8px, 1.4cqw, 16px);
  padding: clamp(8px, 1.1cqw, 14px) clamp(12px, 1.8cqw, 20px);
}

.slider-label,
.slider-value {
  font-size: clamp(11px, 1.35cqw, 15px);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.slider-label { color: var(--ink-soft); }
.slider-value {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  min-width: 5.5ch;
  text-align: right;
  transition: color 300ms ease;
}

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 22px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--accent) 0%, var(--accent) var(--pct, 40%),
    rgba(30, 41, 59, 0.16) var(--pct, 40%), rgba(30, 41, 59, 0.16) 100%);
}
input[type="range"]::-moz-range-track {
  height: 5px;
  border-radius: 999px;
  background: rgba(30, 41, 59, 0.16);
}
input[type="range"]::-moz-range-progress {
  height: 5px;
  border-radius: 999px;
  background: var(--accent);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px; height: 16px;
  margin-top: -5.5px;
  border-radius: 50%;
  background: #fff;
  border: 2.5px solid var(--accent);
  box-shadow: 0 1px 4px rgba(30, 41, 59, 0.28);
  transition: transform 120ms ease;
}
input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: #fff;
  border: 2.5px solid var(--accent);
  box-shadow: 0 1px 4px rgba(30, 41, 59, 0.28);
}
input[type="range"]:hover::-webkit-slider-thumb  { transform: scale(1.12); }
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.02); }

/* ── Arrows ──────────────────────────────────────────────────────────────────
   These used to be pinned to the building's own bounding box, which only works
   while every view is the same picture with a wash over it. Views compose their
   own scenes now and put the building in a different place (or not at all), so
   the arrows sit against the *stage* instead: vertically centred, inset a fixed
   margin from each edge. That holds for any view, and stops them drifting on
   top of a scene's artwork. */
.arrow {
  position: absolute;
  /* 6, above the panels at 4. This lived in a one-line rule further up the
     file which was overridden the moment this block was written — same
     specificity, later wins — so the arrows silently went back under
     everything. The air view's right-hand panel grew far enough to cover the
     next-view arrow outright. */
  z-index: 6;
  top: 50%;
  transform: translate(-50%, -50%);

  display: grid;
  place-items: center;
  width:  clamp(38px, 4.4cqw, 66px);
  height: clamp(38px, 4.4cqw, 66px);
  padding: 0;

  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: 50%;
  backdrop-filter: blur(12px) saturate(1.2);
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease,
              box-shadow 160ms ease, scale 160ms ease;
}
.arrow--prev { left: var(--arrow-inset); }
.arrow--next { left: calc(100% - var(--arrow-inset)); }

.arrow svg {
  width: 44%;
  height: 44%;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.arrow:hover {
  background: #fff;
  color: var(--accent);
  scale: 1.07;
}
.arrow:active { scale: 0.98; }

/* Focus ring, consistent everywhere */
:where(.arrow, .pump-btn, .pump-giveup, .units-opt, input[type="range"]):focus-visible {
  outline: 2.5px solid var(--accent);
  outline-offset: 3px;
}

/* =========================================================================
   Per-view effects  —  ILLUSTRATIVE ONLY
   -------------------------------------------------------------------------
   These are mood layers, not data. Every one is anchored to the --dc-* box,
   and --fx-strength (0→1) comes straight from the slider.

   The light and heat layers were removed with the two views they belonged to.
   The two that remain are inert: --fx-strength is only ever non-zero for a
   view that declares a `slider`, and none does any more. Sound and ecosystem
   replace the plate with scenes of their own, so these would be anchored to
   the wrong artwork even if they did render. Worth deleting outright the next
   time this file is opened, along with the slider chrome.
   ========================================================================= */

/* Plumes off the stacks. Over the plate and over the fx layers — it is the
   one thing on the homepage that is meant to look like it is in the air. */
.hp-smoke {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 2;
  pointer-events: none;
}
.hp-smoke[hidden] { display: none; }

.fx-root { position: absolute; inset: 0; z-index: 2; pointer-events: none; }

.fx {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 450ms ease;
}
/* Only the effect matching the current view is shown */
:root[data-view="sound"]     .fx--sound,
:root[data-view="ecosystem"] .fx--ecosystem {
  opacity: var(--fx-strength);
}

/* Sound — pressure rings rolling outward from the plant */
.fx--sound .ring {
  position: absolute;
  left: var(--dc-cx);
  top: var(--dc-cy);
  width: 40%;
  aspect-ratio: 1.9 / 1;             /* squashed to sit in the isometric plane */
  border: 2px solid rgba(139, 92, 246, 0.75);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  animation: ring-out 3.6s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
}
.fx--sound .ring:nth-child(2) { animation-delay: 1.2s; }
.fx--sound .ring:nth-child(3) { animation-delay: 2.4s; }
@keyframes ring-out {
  0%   { transform: translate(-50%, -50%) scale(0.35); opacity: 0; }
  12%  { opacity: 0.9; }
  100% { transform: translate(-50%, -50%) scale(3.1);  opacity: 0; }
}

/* Ecosystem — the cleared footprint, and what it pushes to the edge */
.fx--ecosystem {
  background: radial-gradient(52% 38% at var(--dc-cx) var(--dc-cy),
              rgba(120, 113, 108, 0.55), transparent 72%);
  mix-blend-mode: multiply;
}
.fx--ecosystem .edge {
  position: absolute;
  left: var(--dc-cx);
  top: var(--dc-cy);
  width: 78%;
  aspect-ratio: 1.9 / 1;
  transform: translate(-50%, -50%);
  border: 2px dashed rgba(16, 185, 129, 0.85);
  border-radius: 50%;
}

/* =========================================================================
   Motion & small screens
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* Portrait phones ─────────────────────────────────────────────────────────
   A 3:2 landscape composition leaves the stage only ~260px tall on a phone,
   which is nowhere near enough to also carry the chrome. So the HUD detaches
   from the artwork and docks to the bottom of the screen instead.

   The stage itself does NOT move, which keeps the intro → homepage handoff
   seamless in portrait too. */
@media (orientation: portrait) and (max-width: 820px) {
  /* A 16:9 frame in a tall window leaves the artwork tiny. The width we added
     to reach 16:9 is pure gradient, so it's safe to crop it back off here —
     the stage goes 3:2 and the image fills it, buying ~20% more artwork.
     The --dc-* anchors are remapped for the crop so the effect layers stay
     glued to the building. */
  .stage, .hud-layer { aspect-ratio: 3 / 2; }
  .stage-media { object-fit: cover; }
  :root {
    --dc-left:  25.8%;
    --dc-right: 73.1%;
    --dc-cx:    49.4%;
  }

  .hud-layer {
    position: fixed;
    inset: auto 0 0 0;
    width: auto;
    aspect-ratio: auto;
    max-height: none;

    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "slider slider slider"
      "prev   pump   next";
    align-items: center;
    gap: 10px;
    padding: 12px;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  }

  /* The homepage note has no cell in the docked bar and would be auto-placed
     into whatever row was going spare. It stays on the artwork instead, above
     the bar, where there is room for it in a 3:2 crop. */
  .hud--intro {
    position: fixed;
    left: 4%; right: 4%;
    bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px));
    width: auto;
  }

  /* Everything goes back into normal flow */
  .hud--slider, .hud--pump, .arrow {
    position: static;
    transform: none;
    inset: auto;
  }
  .hud--slider { grid-area: slider; width: auto; }
  .arrow--prev { grid-area: prev; }
  .arrow--next { grid-area: next; }
  .hud--pump   { grid-area: pump; width: auto; justify-items: center; text-align: center; }

  /* The heading sits over the artwork, which is short on a phone */
  .hud--head { width: 62%; top: 3.5%; }

  .arrow { width: 44px; height: 44px; }

  /* cq units now measure the viewport, so pin the type back to fixed sizes */
  .slider-label, .slider-value { font-size: 13px; }
  .pump-prompt   { font-size: 12px; }
  .pump-btn      { font-size: 14px; }
  .pump-readout  { font-size: 12px; }
  .pump-giveup   { font-size: 11px; }
  .units-opt     { font-size: 11px; }
}

/* =========================================================================
   COMPOSED SCENES
   =========================================================================
   The homepage is one baked plate. A view that needs a different arrangement
   of the same world builds it out of transparent layers instead, stacked over
   the plate and cross-faded in. Layers are positioned in % of the stage, so
   they stay locked together at any window size — the same rule the HUD uses.

   Every scene paints its own background, so it fully replaces the plate
   rather than sitting on top of it. */
.scene {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 420ms ease, visibility 0s linear 420ms;
  pointer-events: none;
}
/* Shown when main.js marks this scene as the current view's (`is-on`) and its
   images have all decoded (`is-ready`). Deliberately not keyed to a specific
   view id — a per-view rule is one more thing to remember when adding a view,
   and forgetting it silently leaves the homepage plate showing through. */
.scene.is-on.is-ready {
  opacity: 1;
  visibility: visible;
  transition: opacity 420ms ease, visibility 0s;
}

.wc-layer {
  position: absolute;
  display: block;
  height: auto;
}
.wc-bg     { inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Fitted to the mockup: the painting's own canvas is a little wider than the
   frame, and it hangs off the left and bottom edges by design.

   The data center sits further left than the mockup places it, and its exact
   spot is not free. The pipe's angle is pinned by the soil it comes out of and
   its length by how big it should read, so the building is what moves to meet
   it, and its slab has to stay over the pipe's cut end (see .wc-pipe-box).
   Raising the building pulls that cover away at ~0.86px per px; sliding it
   left puts it back at ~0.51px per px. So height is bought with left: roughly
   1.7px left for every 1px up. This spot clears the cut by 128px. */
.wc-ground { left: 0;      top: 0.7%;  width: 104.1%; }
.wc-dc     { left: 33.905%; top: 34.629%; width: 34.0%; }

/* ── The pipe, and the water inside it ───────────────────────────────────────
   The canvas and the pipe image share this one box, at the pipe artwork's own
   proportions, so the flow can be authored in the artwork's pixel coordinates
   and still follow it through the transform — the canvas is rotated, clipped
   and faded along with the image, so js/water.js never has to know about any
   of it.

   The angle is a constraint, not a look. -46.17deg is the normal to the soil
   surface where the pipe meets it (fitted over x 1060..1280 of the painting,
   rms 20px), so the inlet sits flush and square in the ground instead of
   skimming along it. `transform-origin` is the inlet flange, the one point the
   rotation leaves fixed, so the mouth stays planted on the seam in the soil
   whatever else changes.

   That angle fixes the rest of the pipe too: the artwork is a Z, and with the
   mouth square to a 44deg slope its middle run comes out parallel to that
   slope, diving back towards the ground. So only the first run is kept —
   `clip-path` drops everything below y=205 — and the cut edge is hidden by
   the data center, which is placed to cover it (see .wc-dc). Nothing about
   this pipe is free: the angle comes from the soil, the length from how big
   it should read, and the building goes wherever those two leave it. */
.wc-pipe-box {
  position: absolute;
  left: 28.624%;
  top: 67.486%;
  width: 23.181%;
  aspect-ratio: 937 / 980;
  transform-origin: 2.135% 8.98%;
  transform: rotate(-46.17deg);
  clip-path: inset(0 0 79.08% 0);
}
.wc-pipe,
.wc-flow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.wc-flow { z-index: 1; }    /* under the outline: the pipe's interior is clear */
.wc-pipe { z-index: 2; }

/* =========================================================================
   VIEW HEADINGS  +  WATER CONTROLS
   ========================================================================= */

@font-face {
  font-family: "Mochibop";
  src: url("../fonts/mochibop.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* Headings borrow the artwork's own face and its painted treatment — grey
   fill, white keyline, soft dropped shadow — measured off datacenterfont.png
   and matched here with paint-order + text-shadow rather than an image, so it
   stays crisp and reflows. */
.hud--head,
.hud--brand {
  background: none;
  border: 0;
  border-radius: 0;
  backdrop-filter: none;
  box-shadow: none;
  pointer-events: none;
  font-family: "Mochibop", ui-rounded, system-ui, sans-serif;
  color: #4e4e4e;
  -webkit-text-stroke: 0.085em #fff;
  paint-order: stroke fill;
  text-shadow: 0.045em 0.10em 0.022em rgba(0, 0, 0, 0.27);
}

.hud--head {
  left: 4.6%;
  top: 5.6%;
  width: 46%;
  margin: 0;
}
.head-title {
  margin: 0;
  font-size: clamp(26px, 5.0cqw, 92px);
  font-weight: 400;
  line-height: 1.04;
}
.head-sub {
  margin: 0.45em 0 0;
  width: 88%;
  font-size: clamp(13px, 1.85cqw, 34px);
  line-height: 1.3;
  -webkit-text-stroke: 0.10em #fff;
}

.hud--brand {
  right: 3.2%;
  top: 3.4%;
  margin: 0;
  font-size: clamp(13px, 1.9cqw, 35px);
  text-align: right;
}
.hud--brand em { font-style: normal; }

/* Headings and the pump only exist for views that declare them */
.hud--head[hidden],
.hud--brand[hidden],
.hud--pump[hidden],
.hud--slider[hidden] { display: none; }

/* ── Pump ────────────────────────────────────────────────────────────────── */
.hud--pump {
  left: 54.6%;
  bottom: 5.5%;
  width: 33%;
  padding: clamp(10px, 1.5cqw, 22px) clamp(12px, 1.8cqw, 26px);

  display: grid;
  justify-items: start;
  gap: clamp(6px, 0.9cqw, 12px);
}

.pump-prompt {
  margin: 0;
  font-size: clamp(12px, 1.45cqw, 24px);
  color: var(--ink-soft);
  min-height: 1.3em;
}
.pump-prompt.is-nudge { color: var(--accent); font-weight: 600; }

.pump-btn {
  font: inherit;
  font-size: clamp(13px, 1.5cqw, 25px);
  font-weight: 600;
  color: #7a2231;
  padding: 0.62em 1.5em;
  border: 1px solid rgba(122, 34, 49, 0.28);
  border-radius: 12px;
  background: linear-gradient(180deg, #fbd3dc, #f6bccb);
  box-shadow: 0 2px 0 rgba(122, 34, 49, 0.18), var(--shadow);
  cursor: pointer;
  touch-action: none;          /* hold-to-pump, not scroll-to-pump */
  user-select: none;
  -webkit-user-select: none;
  transition: transform 90ms ease, box-shadow 90ms ease, filter 150ms ease;
}
.pump-btn:hover { filter: brightness(1.03); }
.pump-btn.is-held {
  transform: translateY(2px);
  box-shadow: 0 0 0 rgba(122, 34, 49, 0.18), var(--shadow);
  filter: brightness(0.98);
}
.pump-btn[disabled] { opacity: 0.55; cursor: default; }

/* Quiet by design: it should be findable without competing with the pump */
.pump-giveup {
  font: inherit;
  font-size: clamp(10px, 1.15cqw, 19px);
  color: var(--ink-soft);
  padding: 0.25em 0;
  margin-top: -0.2em;
  border: 0;
  background: none;
  text-decoration: underline;
  text-underline-offset: 0.22em;
  text-decoration-color: rgba(100, 116, 139, 0.45);
  cursor: pointer;
  transition: color 150ms ease;
}
.pump-giveup:hover { color: var(--ink); }
.pump-giveup[hidden] { display: none; }

.pump-readout {
  margin: 0;
  font-size: clamp(12px, 1.35cqw, 22px);
  color: var(--ink-soft);
  line-height: 1.35;
}
.pump-amount {
  font-size: 1.5em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.pump-unit { font-weight: 600; color: var(--ink); }
.pump-of   { display: block; }

/* ── Units switch ────────────────────────────────────────────────────────── */
/* `display` on a class beats the UA sheet's `[hidden] { display: none }`, so a
   hidden toggle group needs saying twice. The sound view relies on this: its
   place switcher is down to one option and is hidden rather than removed,
   because js/sound.js reads the current place off it. */
.units[hidden] { display: none; }
.units {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  border-radius: 999px;
  background: rgba(30, 41, 59, 0.07);
}
.units-opt {
  font: inherit;
  font-size: clamp(10px, 1.05cqw, 17px);
  font-weight: 600;
  color: var(--ink-soft);
  padding: 0.35em 0.9em;
  border: 0;
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background 150ms ease, color 150ms ease;
}
.units-opt.is-on {
  color: var(--ink);
  background: #fff;
  box-shadow: 0 1px 3px rgba(30, 41, 59, 0.14);
}

/* =========================================================================
   ENERGY VIEW
   =========================================================================
   113,000 houses is far past what the DOM will carry — that many elements is
   hundreds of megabytes of nodes and a page that cannot be scrolled. They go
   onto one canvas instead, stamped from a single pre-rendered sprite, and the
   canvas is never cleared: each frame only draws the houses added that frame.
   So the cost is 113,000 draws *once* (~180ms of work in total, spread over
   the fill), not 113,000 draws per frame.

   What that leaves is a memory question, and it is set by how big a house is
   rather than how many there are — see js/energy.js. */

.eg-layer { position: absolute; inset: 0; width: 100%; height: 100%;
            object-fit: cover; display: block; }

/* The field scrolls inside the stage, so the page itself never moves */
.eg-field {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* .scene turns pointer events off so a scene can never swallow clicks meant
     for the HUD. This one has to take them back, or it cannot be scrolled. */
  pointer-events: auto;
}
/* Fills the field until the houses give it a real height */
.eg-inner { position: relative; width: 100%; min-height: 100%; }
/* Shrink-wraps the canvas (one screen tall) and pins it to the top of the
   scroll container, so the field scrolls past a canvas that stays put.
   Giving this a height would inherit the whole field and stop it sticking. */
.eg-viewport { position: sticky; top: 0; z-index: 0; }
.eg-homes { display: block; }

/* `top` is set from JS: it has to be in px of the visible box, not of the
   scrolling inner, whose height depends on where the building stands. */
.eg-dc { position: absolute; left: 5%; width: 32%; height: auto; display: block; }

/* the sprite source: decoded, never shown */
.eg-house-src { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── Guess panel ─────────────────────────────────────────────────────────── */
.hud--guess {
  left: 38%;
  top: 30%;
  width: 46%;
  padding: clamp(12px, 1.6cqw, 24px) clamp(14px, 2cqw, 28px);
  display: grid;
  gap: clamp(8px, 1.1cqw, 15px);
  justify-items: start;
}
.hud--guess[hidden] { display: none; }

.guess-q {
  margin: 0;
  font-size: clamp(13px, 1.7cqw, 30px);
  font-weight: 600;
  line-height: 1.25;
}
.guess-form { display: flex; align-items: center; gap: clamp(6px, 0.9cqw, 12px); flex-wrap: wrap; }
.guess-label { font-size: clamp(11px, 1.3cqw, 22px); color: var(--ink-soft); }

.guess-input {
  font: inherit;
  font-size: clamp(13px, 1.5cqw, 25px);
  font-variant-numeric: tabular-nums;
  width: clamp(120px, 15cqw, 260px);
  padding: 0.35em 0.6em;
  color: var(--ink);
  background: #fff;
  border: 2px solid rgba(30, 41, 59, 0.75);
  border-radius: 8px;
}
.guess-input:focus { outline: 2.5px solid var(--accent); outline-offset: 2px; }

.guess-go {
  font: inherit;
  font-size: clamp(12px, 1.4cqw, 23px);
  font-weight: 600;
  color: #fff;
  padding: 0.42em 1.1em;
  border: 0;
  border-radius: 9px;
  background: var(--accent);
  cursor: pointer;
  transition: filter 150ms ease;
}
.guess-go:hover { filter: brightness(1.08); }
.guess-go[disabled] { opacity: 0.5; cursor: default; }

.guess-verdict {
  margin: 0;
  min-height: 1.3em;
  font-size: clamp(12px, 1.5cqw, 26px);
  line-height: 1.3;
}
.guess-verdict strong { color: var(--accent); }

.guess-count { margin: 0; font-size: clamp(11px, 1.3cqw, 22px); color: var(--ink-soft); }
.guess-count[hidden] { display: none; }
.guess-n { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }

@media (orientation: portrait) and (max-width: 820px) {
  .hud--guess { left: 4%; top: auto; bottom: 26%; width: 92%; }
}

/* =========================================================================
   AIR POLLUTION VIEW
   ========================================================================= */

.aq-layer { position: absolute; inset: 0; width: 100%; height: 100%;
            object-fit: cover; display: block; }

/* The yard holds the plant and the smoke over it. Everything inside is placed
   by js/air.js on the artwork's own isometric grid, so the units sit in rows
   that recede the way the buildings do rather than on a flat grid.

   The `z-index` is what makes it a stacking context, and it has to be one: the
   yard stacks its contents from 1 to 30, and neither `.scene` nor `.stage` is a
   stacking context, so without one here those numbers escape up to `.layer` and
   outrank the HUD's own 3 — the building and the generators painted over the
   panels. Any value works; 0 keeps the yard where document order already put
   it, over `.aq-bg`. */
.aq-yard { position: absolute; inset: 0; z-index: 0; }
.aq-yard img {
  position: absolute;
  display: block;
  height: auto;
  will-change: transform;
}
/* Above the plant, not below it. The units are placed at z-index 9-14, so at 5
   the plumes were being drawn behind the machines they come out of. */
.aq-smoke { position: absolute; inset: 0; width: 100%; height: 100%; display: block;
            pointer-events: none; z-index: 30; }

/* Sources: decoded, never shown — js/air.js stamps copies of them */
.aq-src { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── The two topic panels ────────────────────────────────────────────────── */
.hud--air {
  bottom: 3.5%;
  width: 46%;
  padding: clamp(10px, 1.4cqw, 22px) clamp(12px, 1.7cqw, 26px);
  display: grid;
  gap: clamp(5px, 0.7cqw, 10px);
  align-content: start;
  /* These grew when the sourced notes landed. Capped and scrollable rather
     than allowed to run up over the artwork on a short window. */
  max-height: 62%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.hud--air[hidden] { display: none; }
.hud--air-left  { left: 3.5%; }
.hud--air-right { right: 3.5%; }

.aq-h {
  margin: 0;
  font-family: "Mochibop", ui-rounded, system-ui, sans-serif;
  font-size: clamp(15px, 2.1cqw, 38px);
  font-weight: 400;
  line-height: 1.05;
  color: var(--ink);
}
.aq-p {
  margin: 0;
  font-size: clamp(10px, 1.15cqw, 19px);
  line-height: 1.35;
  color: var(--ink-soft);
}
.aq-q {
  margin: 0;
  font-size: clamp(10px, 1.2cqw, 20px);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}

/* ── Multiple choice ─────────────────────────────────────────────────────── */
.aq-opts { display: flex; flex-wrap: wrap; gap: clamp(4px, 0.6cqw, 9px); margin-top: 0.15em; }
.aq-opt {
  font: inherit;
  font-size: clamp(10px, 1.1cqw, 18px);
  font-variant-numeric: tabular-nums;
  padding: 0.34em 0.8em;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.72);
  border: 1.5px solid rgba(30, 41, 59, 0.18);
  border-radius: 999px;
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, transform 90ms ease;
}
.aq-opt:hover:not([disabled]) { background: #fff; border-color: rgba(30, 41, 59, 0.4); }
.aq-opt[disabled] { cursor: default; }
.aq-opt.is-right { background: #dcfce7; border-color: #16a34a; font-weight: 700; }
.aq-opt.is-wrong { background: #fee2e2; border-color: #dc2626; opacity: 0.85; }
.aq-opt.is-dim   { opacity: 0.45; }

.aq-row { display: flex; align-items: center; gap: clamp(5px, 0.7cqw, 10px); flex-wrap: wrap; }
.aq-row .guess-input { width: clamp(110px, 12cqw, 210px); }

.aq-verdict {
  margin: 0;
  min-height: 1.2em;
  font-size: clamp(10px, 1.2cqw, 20px);
  line-height: 1.3;
}
.aq-verdict strong { color: var(--accent); }
.aq-note {
  margin: 0;
  font-size: clamp(9px, 1.02cqw, 17px);
  line-height: 1.35;
  color: var(--ink-soft);
}
.aq-note[hidden] { display: none; }
.aq-note strong { color: var(--ink); font-weight: 700; }

/* ── Footnotes ───────────────────────────────────────────────────────────────
   A superscript number that jumps to the matching entry on sources.html and
   carries `?from=<view>` so the back link returns here. Kept small enough not
   to interrupt the line and given a real hit area anyway: `padding` would push
   the line apart, so the target is widened with a transparent outline instead. */
.fn {
  display: inline-block;
  margin-left: 0.12em;
  font-size: 0.68em;
  font-weight: 700;
  line-height: 1;
  vertical-align: super;
  color: var(--accent);
  text-decoration: none;
  outline: 0.35em solid transparent;   /* hit area, no layout cost */
  outline-offset: -0.1em;
}
.fn:hover { text-decoration: underline; text-underline-offset: 2px; }
.fn + .fn { margin-left: 0.22em; }

/* "All sources →", at the foot of a panel that has footnotes in it. */
.pump-src, .aq-all {
  font-size: clamp(9px, 0.95cqw, 15px);
  color: var(--ink-soft);
}
.pump-src { margin: 0.7em 0 0; }
.pump-src a, .aq-all { color: var(--accent); text-decoration: none; }
.pump-src a:hover, .aq-all:hover { text-decoration: underline; text-underline-offset: 2px; }
.aq-all { margin-left: 0.4em; white-space: nowrap; }

/* ── Sourced asides ──────────────────────────────────────────────────────────
   The water and energy views each hold one back until their answer is on
   screen — both name the figure, so showing them early gives away the thing
   the interaction exists to reveal. */
.pump-note, .guess-note {
  margin-top: clamp(8px, 1.1cqw, 18px);
  padding-top: clamp(7px, 0.9cqw, 15px);
  border-top: 1px solid rgba(30, 41, 59, 0.12);
}
.pump-note[hidden], .guess-note[hidden] { display: none; }
.pump-note p, .guess-note p {
  margin: 0;
  font-size: clamp(9px, 1.0cqw, 16px);
  line-height: 1.42;
  color: var(--ink-soft);
}
.pump-note p + p, .guess-note p + p { margin-top: 0.6em; }
.pump-note strong, .guess-note strong { color: var(--ink); font-weight: 700; }

/* Sits under the two that appear on a guess, and is always there. */
.aq-note--always { margin-top: 0.7em; }
/* How the number was arrived at, not what it means — set smaller so it reads
   as a method line rather than as another finding. */
.aq-method { font-size: clamp(8px, 0.88cqw, 14px); opacity: 0.9; }

/* The figure the Colossus note exists to deliver. Underlined in red rather
   than bolded further — the note is already full of bold, so more weight would
   not single it out; a different colour does. `text-decoration` on the element
   itself, not a border, so it follows the text if the line wraps. */
.aq-mark {
  text-decoration: underline;
  text-decoration-color: #dc2626;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
  text-decoration-skip-ink: none;
}

/* The car turns up beside the answer once the guess is in. It lives in the
   panel rather than out on the artwork: the panel grows when the footnote
   appears, and anything parked underneath it gets covered. */
.aq-answer-row { display: flex; align-items: center; gap: clamp(6px, 1cqw, 14px); }
.aq-answer-row .aq-verdict { flex: 1 1 auto; }
.aq-car-slot { flex: 0 0 auto; width: clamp(56px, 8cqw, 130px); }
.aq-car {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
  transform: translateY(8px) scale(0.92);
  transition: opacity 520ms ease, transform 520ms cubic-bezier(.2,.9,.3,1.2);
}
.aq-car.is-shown { opacity: 1; transform: translateY(0) scale(1); }

@media (orientation: portrait) and (max-width: 820px) {
  .hud--air { position: static; width: auto; }
}

/* =========================================================================
   SOUND POLLUTION VIEW
   =========================================================================
   The only view drawn in plan rather than elevation: contours of the building's
   own noise at 25, 50, 100, 250 and 530 ft, with houses standing on the last of
   them, and a slider laid along a diagonal ray out through all five.

   Everything in the plan is authored by js/sound.js in one 1600x900 grid
   (`.sd-plane`, and the SVG's viewBox). The stage is 16:9, so that grid's units
   are square: radii, angles and the slider's rotation are all in the same unit
   and none of them need an aspect correction. The tick labels and the slider
   ride in the HUD layer instead, because they are type and chrome and want to
   scale in `cqw` with the rest of it. */

.sd-plane { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

.sd-layer,
.sd-arcs { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.sd-bg   { object-fit: cover; }

.sd-ring, .sd-wave, .sd-here {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Drawn in the artwork's own ink, at the artwork's own weight */
.sd-ring { stroke: #4b5563; stroke-width: 6.5; opacity: 0.8; }
/* The contour you are standing on, swept along with the slider */
.sd-here { stroke: var(--accent); stroke-width: 8.5; opacity: 0.92; }
/* Pressure rolling outward, only while the audio is actually playing */
.sd-wave { stroke: var(--accent); stroke-width: 4; }

.sd-dc {
  position: absolute;
  display: block;
  height: auto;
  /* The building is painted in the same pale greys as the plate behind it. At
     this size, on this background, it needs a shadow to read at all. */
  filter: drop-shadow(0 6px 16px rgba(30, 41, 59, 0.18));
}

/* Over the contour, so the 530 ft ring reads as running through the
   neighbourhood rather than stopping in front of it. Nudged up so the ring
   passes through the houses' footings rather than their roofs. */
.sd-houses { position: absolute; inset: 0; }
.sd-house {
  position: absolute;
  display: block;
  height: auto;
  translate: -50% -58%;
  filter: drop-shadow(0 2px 3px rgba(30, 41, 59, 0.18));
}
/* the sprite source: decoded, never shown */
.sd-house-src { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ── Tick labels ─────────────────────────────────────────────────────────── */
.sd-marks { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.sd-marks[hidden] { display: none; }
.sd-mark {
  position: absolute;
  translate: -50% -50%;
  font-size: clamp(11px, 1.28cqw, 23px);
  font-weight: 600;
  color: var(--ink-soft);
  white-space: nowrap;
  /* A label sits on painted artwork, and its own contour runs almost straight
     across it — a contour's tangent at the axis is near perpendicular to a line
     of type, so no offset gets a horizontal label clear of it. It is knocked
     out of whatever it crosses instead. */
  text-shadow:
    0 0 4px #fff, 0 0 4px #fff, 0 0 8px #fff,
    0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff;
}

/* ── The diagonal slider ─────────────────────────────────────────────────────
   One rotated box, pinned at the 25 ft end of the ray and as long as the run
   out to 530 ft. js/sound.js writes the four numbers as custom properties
   rather than as inline left/top/width, so the portrait rules below can still
   put the whole thing back into the docked control bar. */
.sd-track {
  position: absolute;
  left:  var(--sd-x, 23%);
  top:   var(--sd-y, 48%);
  width: var(--sd-len, 32%);
  height: 0;
  z-index: 5;
  transform-origin: 0 0;
  transform: rotate(var(--sd-rot, 34deg));
}
.sd-track[hidden] { display: none; }

input[type="range"].sd-slider {
  position: absolute;
  left: 0; top: 0;
  width: 100%;
  height: clamp(28px, 2.8cqw, 46px);   /* the drag target, not the drawn line */
  transform: translateY(-50%);
  cursor: grab;
}
input[type="range"].sd-slider:active { cursor: grabbing; }

/* A dashed path rather than a filled bar: neither end of a distance axis is
   "more", so a progress fill would be saying something untrue. */
input[type="range"].sd-slider::-webkit-slider-runnable-track {
  height: 7px;
  border-radius: 999px;
  background:
    repeating-linear-gradient(90deg, rgba(51, 65, 85, 0.5) 0 9px, transparent 9px 20px),
    rgba(255, 255, 255, 0.5);
}
input[type="range"].sd-slider::-moz-range-track {
  height: 7px;
  border-radius: 999px;
  background:
    repeating-linear-gradient(90deg, rgba(51, 65, 85, 0.5) 0 9px, transparent 9px 20px),
    rgba(255, 255, 255, 0.5);
}
input[type="range"].sd-slider::-moz-range-progress { background: transparent; }

/* The listener */
input[type="range"].sd-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width:  clamp(18px, 1.75cqw, 31px);
  height: clamp(18px, 1.75cqw, 31px);
  margin-top: calc((7px - clamp(18px, 1.75cqw, 31px)) / 2);
  border-radius: 50%;
  background: #fff;
  border: clamp(3px, 0.34cqw, 6px) solid var(--accent);
  box-shadow: 0 1px 5px rgba(30, 41, 59, 0.38);
}
input[type="range"].sd-slider::-moz-range-thumb {
  width:  clamp(18px, 1.75cqw, 31px);
  height: clamp(18px, 1.75cqw, 31px);
  border-radius: 50%;
  background: #fff;
  border: clamp(3px, 0.34cqw, 6px) solid var(--accent);
  box-shadow: 0 1px 5px rgba(30, 41, 59, 0.38);
}

/* It opens at 530 ft, the far end, which is the one position on the track a
   reader has no reason to expect to be the starting one. So it asks to be
   moved, until it has been. */
.sd-track:not(.is-touched) input[type="range"].sd-slider::-webkit-slider-thumb { animation: sd-nudge 2s ease-in-out infinite; }
.sd-track:not(.is-touched) input[type="range"].sd-slider::-moz-range-thumb     { animation: sd-nudge 2s ease-in-out infinite; }
@keyframes sd-nudge {
  0%, 100% { transform: scale(1);    box-shadow: 0 1px 5px rgba(30, 41, 59, 0.38); }
  50%      { transform: scale(1.22); box-shadow: 0 1px 5px rgba(30, 41, 59, 0.38), 0 0 0 7px rgba(124, 58, 237, 0.16); }
}

/* ── The panel ───────────────────────────────────────────────────────────────
   Inset from the right rather than flush to it: the next-view arrow is pinned
   to the middle of the stage's right edge and this panel is tall enough to run
   straight through it. */
.hud--sound {
  right: 9.4%;
  top: 16.5%;
  width: 28%;
  padding: clamp(11px, 1.3cqw, 22px) clamp(12px, 1.55cqw, 25px) clamp(10px, 1.2cqw, 20px);
  display: grid;
  gap: clamp(4px, 0.58cqw, 9px);
  justify-items: start;
  align-content: start;
  /* Grew when the JLARC context and the low-frequency caveat landed. Capped to
     the room between its top edge and the bottom of the stage, and scrollable
     rather than running off the frame — this one is dense enough that on a
     16:9 window the last line or two is still a scroll away. */
  max-height: 82%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.hud--sound[hidden] { display: none; }

.sd-eyebrow { margin: 0; font-size: clamp(10px, 1.0cqw, 17px); color: var(--ink-soft); }
.sd-eyebrow strong { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }

.sd-level { margin: -0.1em 0 0; display: flex; align-items: baseline; gap: 0.3em; }
.sd-dba {
  font-size: clamp(32px, 3.9cqw, 71px);
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.sd-dba-unit { font-size: clamp(11px, 1.25cqw, 21px); font-weight: 600; color: var(--ink-soft); }

/* Quoted only against the outdoor level — it is an outdoor guideline */
.sd-who {
  margin: 0;
  padding: 0.3em 0.8em;
  font-size: clamp(9px, 0.9cqw, 15px);
  font-weight: 600;
  line-height: 1.25;
  color: #92400e;
  background: rgba(251, 191, 36, 0.22);
  border-radius: 999px;
}
.sd-who[hidden] { display: none; }

.sd-like   { margin: 0; font-size: clamp(11px, 1.14cqw, 19px); line-height: 1.3; }
.sd-muffle { margin: 0; font-size: clamp(9px, 0.92cqw, 15px); line-height: 1.35; color: var(--ink-soft); }
.sd-muffle[hidden] { display: none; }

/* ── The volume warning ──────────────────────────────────────────────────────
   Directly above the control it is about, and it stays after the first play
   rather than disappearing: nothing on this page can set the reader's speakers,
   so the calibration is theirs to keep. It just gets quieter once it has been
   read, which is what the second half of the sentence is separated for. */
.sd-warn {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: clamp(6px, 0.85cqw, 13px);
  margin-top: 0.15em;
  padding: clamp(7px, 0.92cqw, 15px) clamp(8px, 1.05cqw, 17px);
  border: 1.5px solid rgba(180, 83, 9, 0.42);
  border-radius: 11px;
  background: rgba(254, 243, 199, 0.75);
  transition: background 320ms ease, border-color 320ms ease;
}
.sd-warn-mark {
  display: grid;
  place-items: center;
  width:  clamp(16px, 1.5cqw, 26px);
  height: clamp(16px, 1.5cqw, 26px);
  border-radius: 50%;
  background: #b45309;
  color: #fff;
  font-size: clamp(10px, 1.05cqw, 18px);
  font-weight: 800;
  line-height: 1;
}
.sd-warn-body { margin: 0; font-size: clamp(10px, 0.98cqw, 16px); line-height: 1.35; color: #7c3d09; }
.sd-warn.is-set { background: rgba(255, 255, 255, 0.55); border-color: rgba(100, 116, 139, 0.3); }
.sd-warn.is-set .sd-warn-body { color: var(--ink-soft); }
.sd-warn.is-set .sd-warn-mark { background: rgba(100, 116, 139, 0.8); }
.sd-warn.is-set .sd-warn-long { display: none; }

/* ── Listen ──────────────────────────────────────────────────────────────── */
.sd-controls {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1.1cqw, 18px);
  flex-wrap: wrap;
  margin-top: 0.15em;
}
.sd-listen {
  font: inherit;
  font-size: clamp(12px, 1.22cqw, 21px);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: #fff;
  padding: 0.5em 1.15em 0.5em 0.95em;
  border: 0;
  border-radius: 11px;
  background: var(--accent);
  box-shadow: 0 2px 0 rgba(76, 29, 149, 0.3), var(--shadow);
  cursor: pointer;
  transition: filter 150ms ease, transform 90ms ease;
}
.sd-listen:hover:not([disabled]) { filter: brightness(1.09); }
.sd-listen:active:not([disabled]) { transform: translateY(1px); }
.sd-listen[disabled] { opacity: 0.6; cursor: default; }
.sd-listen:focus-visible { outline: 2.5px solid var(--accent); outline-offset: 3px; }

.sd-icon { width: 1.05em; height: 1.05em; fill: currentColor; flex: 0 0 auto; }
.sd-icon-pause { display: none; }
.sd-listen.is-playing .sd-icon-play  { display: none; }
.sd-listen.is-playing .sd-icon-pause { display: inline; }

.sd-fallback { margin: 0; font-size: clamp(9px, 0.92cqw, 15px); line-height: 1.35; color: #b91c1c; }
.sd-fallback[hidden] { display: none; }

.sd-note { margin: 0.05em 0 0; font-size: clamp(8px, 0.86cqw, 14px); line-height: 1.4; color: var(--ink-soft); }
.sd-note strong { color: var(--ink); font-weight: 700; }

@media (orientation: portrait) and (max-width: 820px) {
  /* The stage crops to 3:2 there and the artwork cover-fits it, so the plan's
     own box has to be widened by the same amount or the geometry slides off
     the background it was measured against. (16/9) ÷ (3/2) = 1.1852. */
  .sd-plane { left: 50%; width: 118.52%; translate: -50% 0; }

  /* A diagonal needs the artwork it crosses. On a phone the controls are in a
     bar under it, so the slider becomes an ordinary one and the tick labels —
     which only mean anything against the diagonal — go away. The panel's
     readout still names the distance. */
  .sd-track  { position: static; width: auto; height: auto; transform: none; grid-area: slider; }
  input[type="range"].sd-slider { position: static; width: 100%; transform: none; }
  .sd-marks  { display: none; }

  /* Into the middle cell of the docked bar, between the two arrows — the same
     place the water view's pump sits. It carries more than that one does, so it
     is capped and scrolls rather than pushing the artwork off the screen. */
  .hud--sound {
    position: static;
    grid-area: pump;
    width: auto;
    min-width: 0;
    max-height: 42vh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .sd-dba { font-size: 32px; }
  .sd-mark, .sd-like { font-size: 13px; }
  .sd-warn-body, .sd-listen { font-size: 12px; }
  .sd-note, .sd-muffle, .sd-who, .sd-eyebrow { font-size: 11px; }
}

/* =========================================================================
   ECOSYSTEM IMPACT VIEW
   =========================================================================
   A pinboard. The building hangs in a frame in the upper-left corner under the
   heading; the notes about it are pinned out across the board to the right, on
   dashed leaders that run out from *behind* the frame. Five impacts share the
   one frame, and the strip along its bottom — caption, count, stops — is both
   the control and the thing that says there are five.

   Geometry, in the 1600x900 frame the whole site is laid out in:
     frame   x 144 –  640,  y 207 – ~640
     notes   three slots out to the right of it, see SLOTS in js/eco.js
   The leaders are measured at runtime, so those two only have to not collide.
   ========================================================================= */

.ec-layer { position: absolute; inset: 0; width: 100%; height: 100%;
            object-fit: cover; display: block; }

/* ── The frame ───────────────────────────────────────────────────────────── */
/* Left edge at 9%, not less: the site's own view arrows sit at
   `--arrow-inset` (4.6%) and are vertically centred, so anything further left
   than this collides with the one on the way in. */
.ec-frame {
  position: absolute;
  /* `top` leaves room for a two-line subtitle above it. */
  left: 9%; top: 26.5%; width: 31%;
  z-index: 3;
  padding: clamp(6px, 0.8cqw, 13px);
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(30, 41, 59, 0.12);
  border-radius: 7px;
  box-shadow: 0 2px 8px rgba(30, 41, 59, 0.09), 0 16px 34px rgba(30, 41, 59, 0.10);
  pointer-events: auto;
}

/* Two thin cards peeking out behind it. The only job is to say the frame holds
   more than one thing — the same job the stops do, said in the other idiom. */
.ec-frame::before, .ec-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(30, 41, 59, 0.13);
  border-radius: 7px;
  box-shadow: 0 6px 14px rgba(30, 41, 59, 0.06);
}
.ec-frame::before { transform: translate(7px, 8px) rotate(0.6deg); }
.ec-frame::after  { transform: translate(14px, 16px) rotate(1.3deg); opacity: 0.75; }

/* The window itself. `overflow: hidden` is what makes a drag read as one
   impact sliding out and the next sliding in. */
.ec-mat {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: 3px;
  background: linear-gradient(160deg, rgba(241, 245, 249, 0.92), rgba(255, 255, 255, 0.7));
  cursor: grab;
  touch-action: pan-y;
}
.ec-mat.is-dragging { cursor: grabbing; }

/* Everything that moves with a drag, and nothing that doesn't. */
.ec-reel { position: absolute; inset: 0; transform: translate3d(var(--ec-drag, 0px), 0, 0); }
.ec-reel.is-settling { transition: transform 260ms cubic-bezier(.22,.61,.36,1); }

/* Low in the window, leaving headroom for whatever rises off the roofline. */
.ec-dc { position: absolute; left: 17%; top: 40%; width: 66%; height: auto;
         display: block; z-index: 3; }

.ec-glow {
  position: absolute;
  /* Anchored at the roofline and taller than it is wide, so the light climbs
     out of the building the way sky glow actually does rather than haloing it.
     The mask fades its foot out so it does not end in a bulge below the slab. */
  /* Smaller and lower than it was out in the open: the frame clips, and a glow
     cut off by the mat's top edge reads as a mistake rather than as sky. */
  left: 50%; top: 48%;
  width: 44%; height: 68%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
              rgba(253, 224, 71, 0.95), rgba(250, 204, 21, 0.42) 46%,
              rgba(250, 204, 21, 0) 80%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.35) 88%, transparent);
          mask-image: linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.35) 88%, transparent);
  opacity: 0;
  transition: opacity 420ms ease;
  z-index: 2;
  pointer-events: none;
}
.ec-glow.is-on { opacity: 1; }
.ec-glow::after {
  content: '';
  position: absolute;
  left: 50%; bottom: 34%;
  width: 34%; height: 74%;
  transform: translateX(-50%);
  background: linear-gradient(to top,
              rgba(254, 240, 138, 0.55), rgba(254, 240, 138, 0));
  -webkit-mask-image: radial-gradient(60% 100% at 50% 100%, #000, transparent 72%);
          mask-image: radial-gradient(60% 100% at 50% 100%, #000, transparent 72%);
}

.ec-fx { position: absolute; inset: 0; width: 100%; height: 100%;
         display: block; pointer-events: none; }
.ec-fx--under { z-index: 2; }     /* behind the building: ground work, glows */
.ec-fx--over  { z-index: 4; }     /* in front: anything leaving the roofline */

/* ── Caption and slider, both inside the frame ───────────────────────────── */
/* Centred under the artwork rather than split left and right. The name is the
   thing you are looking at, so it gets the weight; the count is bookkeeping
   and sits under it, small. */
.ec-caption {
  margin: clamp(6px, 0.8cqw, 13px) 0 clamp(5px, 0.6cqw, 10px);
  text-align: center;
  line-height: 1.2;
}
.ec-name {
  display: block;
  font-size: clamp(12px, 1.45cqw, 24px);
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.005em;
}
.ec-count {
  display: block;
  margin-top: 0.15em;
  font-size: clamp(9px, 0.86cqw, 14px);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

/* The control. The site already styles `input[type="range"]` globally — track,
   thumb, and a `--pct` fill — off `--accent`, so this does not restyle it, it
   just changes what `--accent` means inside this box. Rewriting the track here
   was the first attempt and it lost: `input[type="range"]` (0,1,1) outranks
   `.ec-range` (0,1,0), so the global rule kept painting purple at the default
   40% fill no matter what this said.

   Why override at all: `--accent` is the slide's colour and changes with every
   slide. A control that restyles itself as you use it reads as part of the
   content rather than as the thing moving it, so the slider stays one fixed
   burnt orange. */
.ec-slider {
  --accent: #c2410c;
  --ec-tick: rgba(194, 65, 12, 0.5);
  position: relative;
  display: grid;
  align-items: center;
  padding: 0 2px;
}

/* Dots behind the track: one per slide, filled up to the current one. Purely a
   read-out — the input above takes every pointer. The 8px inset is half the
   global thumb, so the first and last dot sit under the thumb's own travel. */
.ec-ticks {
  position: absolute;
  left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  padding: 0 8px;
  pointer-events: none;
}
.ec-tick {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(30, 41, 59, 0.22);
  transition: background 220ms ease;
}
.ec-tick.is-on { background: var(--ec-tick); }

/* Only what actually differs from the global range: a grab cursor, and enough
   height that the thumb has somewhere to sit above the ticks. */
input[type="range"].ec-range { position: relative; cursor: grab; }
input[type="range"].ec-range:active { cursor: grabbing; }

/* The hint. Quiet enough to ignore, and it keeps its space when it fades so
   nothing in the frame shifts underneath it. */
.ec-hint {
  margin: clamp(4px, 0.5cqw, 8px) 0 0;
  text-align: center;
  font-size: clamp(8px, 0.72cqw, 12px);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
  opacity: 0.72;
  transition: opacity 500ms ease;
}
.ec-hint.is-done { opacity: 0; }

/* ── Leaders ─────────────────────────────────────────────────────────────── */
/* z-index 1 — under the frame, so each one starts beneath it and appears from
   its edge rather than being drawn across it. */
.ec-leads { position: absolute; inset: 0; width: 100%; height: 100%;
            z-index: 1; pointer-events: none; overflow: visible; }
.ec-leads path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.2;
  stroke-dasharray: 7 9;
  stroke-linecap: round;
  opacity: 0.62;
  animation: ec-flow 1.4s linear infinite;
}
@keyframes ec-flow { to { stroke-dashoffset: -16; } }
.ec-leads circle { fill: var(--accent); opacity: 0.75; }

/* ── The notes ───────────────────────────────────────────────────────────── */
/* A two-column grid down the right of the board, not a set of fixed slots.
   Notes are running prose of unequal length, so any position picked by hand
   overlaps its neighbour as soon as one of them wraps a line further. The
   right edge stops at 8.8% in, clear of the site's next-view arrow. */
.ec-tabs {
  position: absolute;
  /* The left edge is the load-bearing one: the heading box is 46% wide, so a
     subtitle can reach 45.6% before it wraps, and a note starting at 43% would
     be under it. Starting at 46% means the two can never meet whatever the
     subtitle says. The right edge clears the site's next-view arrow. */
  left: 46%; right: 8.8%;
  top: 9.5%; bottom: 3.5%;
  z-index: 6;
  pointer-events: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Bottom-aligned, so the notes always sit directly on top of the photograph
     instead of at the top of the board with a hole between them. Slides carry
     two to five notes, so a top-aligned block leaves a gap that changes size
     with the slide; growing downwards from a fixed floor does not. */
  align-content: end;
  /* Wider between the columns than between the rows: the gutter is what a
     second-column note's leader has to cross, and at row-gap width the string
     is too short to read as one. */
  row-gap: clamp(7px, 0.95cqw, 16px);
  column-gap: clamp(16px, 2.4cqw, 42px);
}
/* Two long notes read better across the full width than squeezed into halves. */
.ec-tabs[data-count="1"], .ec-tabs[data-count="2"] { grid-template-columns: 1fr; left: 55%; }

.ec-tab {
  padding: clamp(7px, 0.9cqw, 15px) clamp(9px, 1.1cqw, 18px);
  align-self: start;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(30, 41, 59, 0.10);
  border-top: 3px solid var(--accent);
  border-radius: 4px 4px 9px 9px;
  box-shadow: 0 3px 10px rgba(30, 41, 59, 0.10), 0 12px 26px rgba(30, 41, 59, 0.07);
  transform: rotate(var(--tilt, 0deg));
}
.ec-tab:nth-child(even) { margin-top: clamp(5px, 0.7cqw, 12px); }
/* Links are the one thing in here you can touch. */
.ec-tab a { color: var(--accent); pointer-events: auto; text-underline-offset: 2px; }
.ec-tab a:hover { color: var(--ink); }

.ec-tab h4 {
  margin: 0 0 0.28em;
  font-size: clamp(10px, 1.18cqw, 20px);
  font-weight: 700;
  line-height: 1.15;
  color: var(--ink);
}
.ec-tab p {
  margin: 0;
  font-size: clamp(9px, 0.92cqw, 16px);
  line-height: 1.38;
  /* The same ink as the homepage note: these are running prose meant to be
     read, not the muted chrome `--ink-soft` is for. */
  color: var(--ink);
}

/* A pulled quote inside a note: indented off the accent rather than set in
   quote marks, which at this size turn into specks. */
.ec-tab .ec-quote {
  display: block;
  margin-top: 0.6em;
  padding-left: 0.7em;
  border-left: 2px solid var(--accent);
  color: var(--ink);
  font-style: italic;
}

/* Where it came from. Muted, small, and always last. */
.ec-tab .ec-src {
  margin-top: 0.55em;
  font-size: clamp(8px, 0.76cqw, 13px);
  line-height: 1.3;
  color: var(--ink-soft);
}
.ec-tab .ec-kind {
  margin-left: 0.4em;
  padding: 0.05em 0.4em;
  font-size: 0.88em;
  border: 1px solid rgba(30, 41, 59, 0.16);
  border-radius: 3px;
  white-space: nowrap;
}
.ec-tab .ec-sep { margin: 0 0.45em; opacity: 0.5; }
.ec-tab strong { color: var(--ink); }

/* ── The photograph in the corner ────────────────────────────────────────── */
/* A cell in the notes' grid, always the last one, sitting on the floor of its
   row — so the photograph and the note beside it end on the same line. */
.ec-plate {
  position: relative;
  align-self: end;
  pointer-events: none;
}
/* One note or two puts the grid into a single wide column, and a photograph
   the width of that column is a poster. Held to the right, at print size. */
.ec-tabs[data-count="1"] .ec-plate,
.ec-tabs[data-count="2"] .ec-plate { justify-self: end; width: 52%; }
.ec-tabs[data-count="1"] .ec-plate--cutout,
.ec-tabs[data-count="2"] .ec-plate--cutout { width: 58%; }

/* A mounted print: even white border, a degree off square, and a shadow that
   says it is lying on the board rather than printed onto it. */
.ec-photo {
  margin: 0;
  padding: 4.5%;
  background: #fff;
  border: 1px solid rgba(30, 41, 59, 0.10);
  border-radius: 3px;
  box-shadow: 0 3px 10px rgba(30, 41, 59, 0.12), 0 14px 30px rgba(30, 41, 59, 0.09);
  transform: rotate(1.5deg);
}
.ec-photo img { display: block; width: 100%; height: auto; border-radius: 2px; }

/* Where the picture came from. Set in the mount for a print, and on its own
   line under the pair for the cutouts, which have no mount to sit in. */
.ec-cred {
  margin: 0.42em 0 0;
  font-size: clamp(7px, 0.66cqw, 11px);
  line-height: 1.25;
  color: var(--ink-soft);
  text-align: right;
}
.ec-cred--pair { position: absolute; right: 0; bottom: -1.6em; margin: 0; }
.ec-cred a { color: inherit; pointer-events: auto; text-decoration: underline;
             text-underline-offset: 2px; text-decoration-color: rgba(30, 41, 59, 0.3); }
.ec-cred a:hover { color: var(--accent); text-decoration-color: currentColor; }

/* The cutouts are not prints — they have no edges to mount, so they are
   layered straight onto the board and given their own shadows. The box is an
   aspect ratio so the two can be placed against each other rather than against
   whatever height the images happened to be. */
/* Wider and taller than a mounted print gets: a cutout has no white border to
   read as an edge, so at the print's size it looks smaller than one. The
   aspect ratio is what holds the two apart — tuned so the swallow clears the
   cow's back and overlaps only its hindquarters. */
.ec-plate--cutout { aspect-ratio: 3 / 2; }
.ec-cut {
  position: absolute;
  display: block;
  filter: drop-shadow(0 5px 10px rgba(30, 41, 59, 0.22));
}
.ec-cut img { display: block; width: 100%; height: auto; }
.ec-cut--0 { left: 0;  bottom: 0; width: 80%; }    /* the cow, on the ground */
.ec-cut--1 { right: 0; top: 0;    width: 39%; }    /* the swallow, above it */

/* A claim that traces to one person and has never been tested does not get to
   look like the peer-reviewed note next to it. */
.ec-tab--contested { border-top-color: #ca8a04; background: rgba(254, 252, 232, 0.95); }
.ec-tab--contested h4 { color: #854d0e; }
.ec-tab--counter { border-top-color: #0d9488; }

/* Nothing written here yet. Deliberately drawn as a gap rather than as a note,
   so it reads as unfinished instead of as content. */
.ec-tab--pending {
  background: rgba(255, 255, 255, 0.45);
  border-style: dashed;
  border-top-color: rgba(30, 41, 59, 0.2);
  box-shadow: none;
}
.ec-tab--pending p { font-style: italic; opacity: 0.7; }

/* ── Portrait ────────────────────────────────────────────────────────────── */
/* No room to pin anything beside the frame, so the board becomes a column: the
   frame across the top, the notes stacked and scrolling under it. The leaders
   have nothing left to point along, so they go. */
@media (orientation: portrait) and (max-width: 820px) {
  .ec-frame { left: 4%; top: 15%; width: 92%; }
  .ec-frame::before, .ec-frame::after { display: none; }
  .ec-leads { display: none; }

  .ec-tabs {
    inset: 62% 0 2% 0;
    left: 0; right: 0;
    grid-template-columns: 1fr;
    gap: 9px;
    padding: 0 4%;
    overflow-y: auto;
    overscroll-behavior: contain;
    pointer-events: auto;
  }
  .ec-tabs[data-count="1"], .ec-tabs[data-count="2"] { left: 0; }
  .ec-tab { transform: none; margin-top: 0; }

  /* No corner to pin into once the board is a column: the photograph sits
     above the notes rather than beside them. */
  .ec-plate {
    inset: 46% 0 auto 0;
    right: auto;
    width: 46%;
    margin: 0 auto;
  }
}

/* =========================================================================
   BACKGROUND TREATMENT  —  vibrancy + grain, applied to every view
   =========================================================================
   Two knobs, both here, both CSS-only. No asset is regenerated and no markup
   changes, which matters for one reason in particular: the intro hands off to a
   still that IS the video's last frame, so anything done to one has to be done
   to the other. Both live inside `.stage`, so `.stage::after` covers the video
   and the plate with a single rule and the handoff stays invisible.

   WHY A WASH AND NOT `filter: saturate()`. The painted gradient is a couple of
   points of chroma off white — the pink corner is rgb(255,241,255). `saturate()`
   scales the chroma that is already there, so at 1.3 it moves that pixel by two
   values and at 3.0 it clips two channels and shifts the hue before it is
   visibly more colourful. There is nothing there to amplify. So the colour is
   *added*: a multiply wash in the artwork's own palette, over the top. The
   painting's structure survives underneath it; the saturation of it is now a
   number in this file rather than something baked into a webp.
   ========================================================================= */

:root {
  /* The wash, in the artwork's own hues — warm pink at the corners, sky and a
     touch of mint through the middle, following the painting underneath. */
  --wash: linear-gradient(146deg,
            #ffc9e6 0%,
            #e2ebff 34%,
            #c9eef6 58%,
            #eadcff 80%,
            #ffd2ec 100%);
  --wash-strength: 0.38;   /* 0 switches it off entirely */

  --grain:       0.53;     /* film grain: this is the layer's opacity, and the
                              noise it fades in is nearly white — see below */
  --grain-blend: multiply;
  --grain-size:  220px;

  /* feTurbulence, stitched so the tile has no seam, desaturated and forced
     fully opaque — turbulence writes noise into the alpha channel too, and left
     alone that averages the grain away to almost nothing.
       The transfer maps the noise into [0.42, 1.42], i.e. mostly-white with
     darker specks, and the layer is then faded to 55%. Splitting it that way is
     what keeps grain and darkening independent: `multiply` is the only blend
     with anywhere to go on a background this pale, and multiplying by mid-grey
     to get texture would have cost 20% of the brightness to buy it. Mostly
     white noise at high opacity gives the same tooth for 2%. */
  --grain-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='1' intercept='0.42'/%3E%3CfeFuncG type='linear' slope='1' intercept='0.42'/%3E%3CfeFuncB type='linear' slope='1' intercept='0.42'/%3E%3CfeFuncA type='discrete' tableValues='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* Over the artwork, under the chrome. `.hud-layer` carries z-index 3 against
   this stage's `auto`, so the panels and the type stay untinted. */
.stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  background: var(--wash);
  mix-blend-mode: multiply;
  opacity: var(--wash-strength);
}

/* The letterbox fill sits outside the stage, so it takes the same wash as an
   extra background layer of its own rather than as an overlay — otherwise the
   sliver top and bottom of a 16:10 window stays pale while the artwork deepens. */
.backdrop,
.backdrop--intro {
  background-image: var(--wash), var(--backdrop-src);
  background-position: center, center;
  background-size: 100% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  background-blend-mode: multiply, normal;
}
.backdrop        { --backdrop-src: url("../media/backdrop.jpg"); }
.backdrop--intro { --backdrop-src: url("../media/backdrop-intro.jpg"); }

/* ── Grain ───────────────────────────────────────────────────────────────────
   A tiled, stitched feTurbulence rasterised once — no image file, no request,
   and the whole thing is one pseudo-element, so there is no markup to add and
   nothing for a new view to remember.

   It sits over the chrome as well as the artwork. That is deliberate: frosted
   glass with a little tooth in it reads as glass, and at this opacity it costs
   the type nothing. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  opacity: var(--grain);
  mix-blend-mode: var(--grain-blend);
  background-image: var(--grain-img);
  background-size: var(--grain-size) var(--grain-size);
}
