/* ==========================================================================
   Dutch IX Labs — night.css
   SHEET 00 / NACHT. The components.

   Colour lives in tokens-night.css and nowhere else. Nothing here is allowed a
   raw hex, a radius above 0, or a shadow — depth on this page is light and
   hairline, exactly as it is on the daylight sheet, arrived at from the other
   direction.

   Order:
     1. Chrome — nav, footer
     2. The stage
     3. The grade
     4. The load state
     5. Type on the stage
     6. The register
     7. The dossier
     8. Propositions
     9. Booking
    10. Buttons, chips
    11. Fallbacks
    12. Responsive
   ========================================================================== */


/* ==========================================================================
   1. CHROME
   ========================================================================== */

.night-nav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding: var(--s-4) var(--pad-x);
  background-color: color-mix(in srgb, var(--c-ground) 86%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  border-block-end: 1px solid var(--c-hair);
}

.night-nav__mark { display: grid; gap: 2px; color: var(--c-text); }

.night-nav__name {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 78, "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-sm);
}

.night-nav__end { display: flex; align-items: center; gap: var(--s-5); }

.night-nav__link {
  font-size: var(--fs-sm);
  color: var(--c-text-3);
  border-block-end: 1px solid transparent;
  transition: color var(--t-2) var(--ease-rack), border-color var(--t-2) var(--ease-rack);
}
.night-nav__link:hover { color: var(--c-signal); border-block-end-color: var(--c-signal); }

.lang { display: flex; border: 1px solid var(--c-hair); }
.lang button {
  padding: var(--s-1) var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-eyebrow);
  color: var(--c-text-4);
  min-block-size: 1.75rem;
}
.lang button[aria-pressed="true"],
.lang button.is-active { background-color: var(--c-signal); color: var(--c-on-accent); }

.night-foot {
  padding: var(--s-7) var(--pad-x) var(--s-8);
  border-block-start: 1px solid var(--c-hair);
  display: grid;
  gap: var(--s-3);
}
.night-foot a { color: var(--c-signal); }


/* ==========================================================================
   1b. THE BAR
   Five destinations and one action, in one <nav> at every width. Below the
   breakpoint the same element becomes a panel behind a button — not a second
   copy of the links, because two navs is two things to keep in step and the
   one a screen reader finds is always the stale one.
   ========================================================================== */

.topnav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--s-5);
  min-block-size: var(--nav-h);
  padding-inline: var(--pad-x);
  background-color: color-mix(in srgb, var(--c-ground) 86%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  border-block-end: 1px solid var(--c-hair);
}

.topnav__mark { color: var(--c-text); flex: none; }

.topnav__name {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 78, "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-sm);
}

.topnav__menu {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  min-inline-size: 0;
}

.topnav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 1.6vw, 2rem);
  list-style: none;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}

.topnav__link {
  display: block;
  padding-block: var(--s-2);
  font-size: var(--fs-sm);
  color: var(--c-text-3);
  white-space: nowrap;
  border-block-end: 1px solid transparent;
  transition: color var(--t-2) var(--ease-rack), border-color var(--t-2) var(--ease-rack);
}
.topnav__link:hover { color: var(--c-text); border-block-end-color: var(--c-rule); }

/* Where you are. Set from an IntersectionObserver, and carried by
   aria-current as well as by colour — which one am I on is information, and a
   colour is not information to everybody. */
.topnav__link.is-here { color: var(--c-signal); border-block-end-color: var(--c-signal); }

.topnav__end { display: flex; align-items: center; gap: var(--s-4); flex: none; }
/* --c-lab is per-LAB, and the bar belongs to no lab: outside a [data-lab]
   scope it falls back to the neutral, which rendered the one call to action on
   the page as a beige slab. The bar's accent is the site's own signal. */
.topnav__cta {
  padding: var(--s-2) var(--s-4);
  font-size: var(--fs-sm);
  min-block-size: 2.25rem;
  background-color: var(--c-signal);
  color: var(--c-on-accent);
}
.topnav__cta:hover { background-color: var(--c-text); color: var(--c-ground); }

/* The toggle. Present in the markup at every width and shown only where the
   links do not fit, so a rotation from landscape to portrait cannot leave a
   bar with neither. */
.topnav__toggle {
  display: none;
  flex: none;
  margin-inline-start: auto;
  inline-size: var(--target-min);
  block-size: var(--target-min);
  border: 1px solid var(--c-rule);
  background-color: transparent;
  color: var(--c-text-2);
  position: relative;
}
.topnav__toggle:hover { border-color: var(--c-signal); color: var(--c-signal); }

/* Three rules drawn from one element: the bar itself plus its two
   pseudo-elements. Three <span>s would say the same thing in more markup. */
.topnav__bars,
.topnav__bars::before,
.topnav__bars::after {
  position: absolute;
  inline-size: 1.125rem;
  block-size: 1px;
  background-color: currentColor;
  transition: transform var(--t-3) var(--ease-land), opacity var(--t-2) var(--ease-rack);
}
.topnav__bars {
  inset-block-start: 50%;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
}
.topnav__bars::before { content: ""; inset-inline-start: 0; transform: translateY(-6px); }
.topnav__bars::after  { content: ""; inset-inline-start: 0; transform: translateY(6px); }

.is-open .topnav__bars { background-color: transparent; }
.is-open .topnav__bars::before { transform: rotate(45deg); }
.is-open .topnav__bars::after  { transform: rotate(-45deg); }

@media (max-width: 980px) {
  .topnav { flex-wrap: wrap; }
  .topnav__toggle { display: block; }

  /* Closed is display:none, not opacity or a transform off-screen. A panel
     that is merely invisible still takes tab stops, and the first thing a
     keyboard user meets on a phone should not be five links they cannot see. */
  .topnav__menu {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-4);
    padding-block: var(--s-4) var(--s-5);
    border-block-start: 1px solid var(--c-hair);
    max-block-size: calc(100svh - var(--nav-h));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .topnav.is-open .topnav__menu { display: flex; }

  .topnav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .topnav__link {
    padding-block: var(--s-3);
    font-size: var(--fs-md);
    border-block-end: 1px solid var(--c-hair);
  }
  .topnav__link.is-here { border-inline-start: 2px solid var(--c-signal); padding-inline-start: var(--s-3); }

  .topnav__end { justify-content: space-between; }
  .topnav__cta { flex: 1; text-align: center; }
}

/* The panel covers the page, so the page must not scroll behind it. */
.is-nav-open { overflow: hidden; }

.night-foot__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
}
.night-foot__nav a { font-size: var(--fs-sm); color: var(--c-text-3); }
.night-foot__nav a:hover { color: var(--c-signal); }


/* ==========================================================================
   2. THE STAGE
   ========================================================================== */

.stage {
  position: relative;
  block-size: 100svh;
  min-block-size: 40rem;
  overflow: hidden;
  background-color: var(--c-void);
}

.stage__canvas {
  position: absolute;
  inset: 0;
  z-index: var(--z-canvas);
  opacity: 0;
  transition: opacity var(--t-5) var(--ease-rack);
  cursor: crosshair;
}
.stage__canvas canvas { inline-size: 100%; block-size: 100%; display: block; }

.is-3d .stage__canvas { opacity: 1; }


/* ==========================================================================
   2b. MAP LABELS
   A mark on the map is a tick, a rule and a name — the same three parts the
   daylight sheet uses, redrawn in light. They are DOM so the type stays crisp,
   and they are written from the projected 3D position inside the render hook,
   so they move WITH the camera rather than one frame behind it.
   ========================================================================== */

.marks { position: absolute; inset: 0; z-index: var(--z-hud); pointer-events: none; }

/* A mark on the right of its site reads left-to-right; a mark on the left has
   to be mirrored, or the tick points away from the thing it is pointing at. */
.mark.mark--end { flex-direction: row-reverse; transform: translate3d(var(--x), var(--y), 0) translateX(-100%); }
.mark.mark--end .mark__stack { text-align: end; justify-items: end; }

.mark {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  transform: translate3d(var(--x), var(--y), 0);
  opacity: 0;
  transition: opacity var(--t-3) var(--ease-rack);
  will-change: transform;
  pointer-events: auto;
  cursor: pointer;
}
.mark:hover .mark__name { color: var(--c-lab); }
.mark.is-on { opacity: 1; }

.mark__tick {
  inline-size: 1.75rem;
  block-size: 1px;
  background-color: var(--c-lab);
  flex: none;
}

.mark__stack { display: grid; gap: 2px; }

.mark__name {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 82, "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-sm);
  line-height: 1;
  color: var(--c-text);
  white-space: nowrap;
  text-shadow: 0 0 12px var(--c-void), 0 0 3px var(--c-void);
}

.mark__meta {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-mono);
  color: var(--c-lab);
  white-space: nowrap;
}

.mark--active .mark__tick { block-size: 2px; }

@media (max-width: 900px) { .mark__meta { display: none; } }


/* ==========================================================================
   3. THE GRADE
   Four layers, in this order, all pointer-events:none, above the canvas and
   below the UI. The grain is STATIC — animated grain is a per-frame battery
   cost that reads as video noise rather than film stock.
   ========================================================================== */

.grade { position: absolute; inset: 0; z-index: var(--z-grade); pointer-events: none; }
.grade > * { position: absolute; inset: 0; }

.grade__vignette { background: var(--grade-vignette); }

.grade__grain {
  opacity: var(--grade-grain);
  mix-blend-mode: overlay;
  background-image: var(--media-grain-image);
  background-size: 180px 180px;
}

/* The one thing in the grade that touches the type area, and it is 5%. */
.grade__halation {
  --x: 50%;
  --y: 50%;
  opacity: 0;
  mix-blend-mode: screen;
  background: radial-gradient(40vw 40vw at var(--x) var(--y),
              color-mix(in srgb, var(--c-lab, var(--c-signal)) 22%, transparent) 0%,
              transparent 70%);
  transition: opacity var(--t-5) var(--ease-rack);
}

/* The cinema is the MOVE, not the frame. Height 0 at rest; permanent bars are
   a costume you put on a page. */
.grade__bar {
  inset: auto 0;
  block-size: 0;
  background-color: var(--c-void);
  transition: block-size var(--t-4) var(--ease-rack);
}
.grade__bar--top { inset-block-start: 0; }
.grade__bar--bottom { inset-block-end: 0; }

.is-flying .grade__bar { block-size: max(0px, calc((100svh - 100vw / 2.39) / 2)); }


/* ==========================================================================
   4. THE LOAD STATE
   No spinner, no percentage, no logo. The country is drawn as a 1px signal
   outline and cross-dissolves into the built slab: the loading state and the
   brand are the same object.
   ========================================================================== */

.stage__load {
  position: absolute;
  inset: 0;
  z-index: var(--z-canvas);
  display: grid;
  place-items: center;
  gap: var(--s-5);
  color: var(--c-signal);
  transition: opacity var(--t-5) var(--ease-rack);
}
.stage__load svg {
  inline-size: min(38vw, 46svh);
  block-size: auto;
  opacity: 0.75;
  animation: draw 2.4s var(--ease-rack) infinite alternate;
}
.stage__load p { position: absolute; inset-block-end: 18%; }

.is-3d .stage__load { opacity: 0; }

@keyframes draw {
  from { opacity: 0.35; }
  to   { opacity: 0.85; }
}

@media (prefers-reduced-motion: reduce) {
  .stage__load svg { animation: none; }
}


/* ==========================================================================
   5. TYPE ON THE STAGE
   The title card's baseline sits on the real 3D horizon: JS projects the
   slab's far edge on load and resize and writes --horizon-y. It costs one
   projection and it makes the DOM and the scene look like one image.
   ========================================================================== */

.stage__type {
  position: absolute;
  inset-inline-start: var(--pad-x);
  inset-block-start: calc(var(--nav-h) + 7svh);
  z-index: var(--z-content);
  inline-size: min(30ch, 30vw);
  transition: opacity var(--t-4) var(--ease-rack), transform var(--t-5) var(--ease-rack);
}

.title-card {
  font-family: var(--font-display);
  /* Its own clamp, not --fs-d3. The claim names a thing —
     "infrastructure", "Interaction" — and a 15-letter word set at 72px
     condensed is wider than any sane measure, so the reset's
     overflow-wrap:break-word snaps it mid-word. Smaller type and a wider
     measure, and the words stay whole. */
  font-size: clamp(2.25rem, 1.30rem + 2.4vw, 4rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 900;
  font-weight: 900;
  line-height: 0.94;
  letter-spacing: var(--track-d1);
  color: var(--c-text);
  text-wrap: balance;
  overflow-wrap: normal;
  hyphens: manual;
  max-inline-size: 16ch;
  margin-block: var(--s-3) var(--s-4);
}

.stage__sub {
  font-size: var(--fs-lg);
  line-height: var(--lh-lead);
  color: var(--c-text-3);
  max-inline-size: 38ch;
}

/* When a lab is chosen the picture is the subject, so the claim steps out of
   the way. It moves; it never dissolves into nothing while the camera holds. */
.is-focused .stage__type { opacity: 0; transform: translateX(-1.5rem); pointer-events: none; }

.eyebrow { color: var(--c-text-4); }

.instrument {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-mono);
  line-height: var(--lh-mono);
  font-variant-numeric: tabular-nums;
  color: var(--c-instrument);
}

/* The one strip of cold type on the page. Warm type argues; cold type
   measures. The lens value is LIVE — it prints the camera's real focal length
   during a flight and is never faked. */
.stage__instrument {
  position: absolute;
  inset-block-end: var(--s-4);
  inset-inline: var(--pad-x);
  z-index: var(--z-hud);
  display: flex;
  justify-content: space-between;
  gap: var(--s-5);
  pointer-events: none;
}


/* ==========================================================================
   6. THE REGISTER
   The canvas is never the accessible object. This list is the source of truth
   for the keyboard, for a screen reader, and for a browser with no WebGL.
   Roving tabindex: five labs cost one tab stop, not five.
   ========================================================================== */

.register {
  position: absolute;
  inset-block-start: calc(var(--nav-h) + 4svh);
  inset-inline-end: var(--pad-x);
  z-index: var(--z-content);
  inline-size: min(21rem, 26vw);
}

.register__title {
  margin-block-end: var(--s-3);
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
}
.register__title::before {
  content: "";
  flex: 1;
  block-size: 1px;
  background-color: var(--c-hair);
}

/* A column on the right, not a strip along the base.
   The strip read well but it sat across the south of the country, so the
   BRIX and SHINE labels landed on top of it — and a map label overlapping a
   button is the one collision a map cannot have. A right column also gives
   the frame its three parts: claim left, country centre, index right. */
.register__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  border-block-start: 1px solid var(--c-hair);
}

.register__button {
  inline-size: 100%;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-1) var(--s-3);
  align-content: start;
  padding: var(--s-3) var(--s-3) var(--s-4);
  text-align: start;
  min-block-size: var(--target-min);
  border-block-end: 1px solid var(--c-hair);
  border-inline-start: 2px solid transparent;
  transition: background-color var(--t-2) var(--ease-rack),
              border-color var(--t-2) var(--ease-rack),
              padding-inline-start var(--t-2) var(--ease-rack);
}

.register__num { grid-row: 1; color: var(--c-lab); }
.register__count { grid-row: 1; grid-column: 2; color: var(--c-text-4); justify-self: end; }

.register__name {
  grid-row: 2;
  grid-column: 1 / -1;
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 600;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-md);
  color: var(--c-text);
  line-height: 1.1;
}

.register__place { grid-row: 3; grid-column: 1 / -1; color: var(--c-text-4); }

.register__button:hover,
.register__button:focus-visible {
  background-color: color-mix(in srgb, var(--c-lab) 10%, transparent);
  border-inline-start-color: var(--c-lab);
  padding-inline-start: var(--s-4);
}

.register__button[aria-current="true"] {
  background-color: color-mix(in srgb, var(--c-lab) 18%, transparent);
  border-inline-start-color: var(--c-lab);
  padding-inline-start: var(--s-4);
}

.register__hint { margin-block-start: var(--s-3); }

/* Each row carries its lab's accent, which is what makes the list read as the
   same five objects that are standing on the country. */
.register__item:nth-child(1) .register__button { --c-lab: var(--c-ix3); }
.register__item:nth-child(2) .register__button { --c-lab: var(--c-mix); }
.register__item:nth-child(3) .register__button { --c-lab: var(--c-prime); }
.register__item:nth-child(4) .register__button { --c-lab: var(--c-brix); }
.register__item:nth-child(5) .register__button { --c-lab: var(--c-shine); }

.is-focused .register { opacity: 0.6; }
.is-focused .register:hover, .is-focused .register:focus-within { opacity: 1; }

.stage__back {
  position: absolute;
  inset-block-start: calc(var(--nav-h) + var(--s-5));
  inset-inline-start: var(--pad-x);
  z-index: var(--z-content);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--c-rule);
  color: var(--c-text-2);
  font-size: var(--fs-sm);
  min-block-size: var(--target-min);
  background-color: color-mix(in srgb, var(--c-ground) 70%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur));
  backdrop-filter: blur(var(--panel-blur));
}
.stage__back:hover { border-color: var(--c-signal); color: var(--c-signal); }


/* ==========================================================================
   6b. INSIDE THE LAB
   three.js builds the room; this puts the words on its walls. The interior
   camera is authored and never moves, so the two side regions can simply be
   rotated in CSS to lie on the left and right walls — no per-frame projection,
   and the type stays real text at every pixel ratio.

   THREE SURFACES, IN AN ORDER. The rotation used to be the only thing
   explaining the layout, and it explained the wrong thing: it said "this is a
   room" when the question a visitor arrives with is "what is here and what can
   I book". Each surface now carries a number and a name, so the arrangement is
   an order you read — 01 the lab, 02 what it sells, 03 what stands in it — and
   the perspective reinforces that order instead of competing with it.

   Below the stacking breakpoint the perspective comes off entirely and the
   three surfaces become three tabs. Not a 2,200px scroll: a phone visitor who
   opened one lab had to travel most of a metre of glass to reach the exit, and
   there was no way to tell from the top that there was anything below.
   ========================================================================== */

.room {
  /* What 100svh has to give up before the three surfaces get what is left:
     the room's own bar, plus the block padding on .room__surfaces. Named
     because two rules size themselves against it and a number repeated in two
     places is a number that will disagree with itself. */
  --room-chrome: 5.5rem;

  position: absolute;
  inset: 0;
  z-index: var(--z-content);
  display: grid;
  /* One row for the bar, one for the (hidden on desktop) tabs, one for the
     surfaces. The surfaces row takes the rest, and it is `minmax(0, 1fr)` so
     that its own children are allowed to be shorter than their content and
     scroll — an `auto` row here silently grows the grid past the stage. */
  grid-template-rows: auto auto minmax(0, 1fr);
  animation: room-in var(--t-5) var(--ease-land);
}

@keyframes room-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .room { animation: none; }
}

/* ---- the bar. Out, where you are, close.

   It is a GRID ROW, not three absolutely positioned buttons. The old bar was
   absolute inside .room and the splat backdrop rule then reset it to
   position:relative to give it a stacking order — which dropped both controls
   to the bottom of a 2,289px scroll box on a phone, where nobody found them.
   A row cannot be knocked out of place by a z-index fix. */
/* Inside a lab, THIS is the navigation.
   The global bar slides away while the room is open and the room's own bar
   takes the top of the screen. Two bars stacked cost 128px of a 900px window,
   which is what made the floor 80% of the screen when it was asked to be 90%
   — and the top one was offering to take you somewhere else at the moment you
   had just arrived somewhere. Leaving is still one click, in the bar that
   replaced it, and the global bar comes straight back on the way out. */
.is-inside .topnav { transform: translateY(-100%); }
.topnav { transition: transform var(--t-4) var(--ease-land); }

/* …except for a keyboard user who is inside it. A bar that slides away with
   focus in it is a focus trap you cannot see. */
.is-inside .topnav:focus-within { transform: none; }

.room__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--pad-x);
  border-block-end: 1px solid var(--c-hair);
  background-color: color-mix(in srgb, var(--c-ground) 82%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur));
  backdrop-filter: blur(var(--panel-blur));
  /* Above the surfaces in paint order, so a wall that scrolls under it is
     clipped by the bar rather than drawn over it. */
  position: relative;
  z-index: 2;
}

.room__out {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  min-block-size: var(--target-min);
  border: 1px solid var(--c-rule);
  color: var(--c-text-2);
  font-size: var(--fs-sm);
  background-color: color-mix(in srgb, var(--c-ground) 70%, transparent);
  transition: border-color var(--t-2) var(--ease-rack), color var(--t-2) var(--ease-rack);
}
.room__out:hover { border-color: var(--c-lab); color: var(--c-lab); }
.room__out-arrow { color: var(--c-lab); }

/* Where you are. The short name and the city, because the full legal name is
   on the surface two centimetres below and printing it twice is not emphasis.
   It is the first thing to go when the bar runs out of room: the two controls
   either side of it are the ones you cannot do without. */
.room__here {
  color: var(--c-lab);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-inline-size: 0;
}

/* The same exit, in the corner people already look in for one. Two controls
   for one action is not duplication here — the worded one tells a first-time
   visitor what leaving means, the cross is for everyone after that. */
.room__cross {
  flex: none;
  inline-size: var(--target-min);
  block-size: var(--target-min);
  border: 1px solid var(--c-rule);
  color: var(--c-text-2);
  font-size: var(--fs-md);
  line-height: 1;
  background-color: color-mix(in srgb, var(--c-ground) 70%, transparent);
  transition: border-color var(--t-2) var(--ease-rack), color var(--t-2) var(--ease-rack);
}
.room__cross:hover { border-color: var(--c-lab); color: var(--c-lab); }

/* ---- the tab strip. Built at every width by app.js and hidden here, because
   a strip built only under a media query disappears on a rotation from
   portrait to landscape and never comes back without a resize listener that
   has to be right. */
.room__tabs { display: none; }

.room__tab {
  flex: 1 1 0;
  min-inline-size: 0;
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: var(--s-2) var(--s-1);
  min-block-size: var(--target-min);
  font-size: var(--fs-sm);
  color: var(--c-text-3);
  border-block-end: 2px solid transparent;
  background-color: transparent;
  transition: color var(--t-2) var(--ease-rack), border-color var(--t-2) var(--ease-rack);
}
.room__tab-n { color: var(--c-text-4); }
.room__tab[aria-selected="true"] { color: var(--c-text); border-block-end-color: var(--c-lab); }
.room__tab[aria-selected="true"] .room__tab-n { color: var(--c-lab); }

/* ---- the three surfaces */
.room__surfaces {
  display: grid;
  /* FIXED RATIOS. Every track is minmax(0, …), so no track can be widened by
     what happens to be inside it: a bare `0.92fr` still has a min-content
     floor, which meant a long partner list or a wide chip pushed a wall out
     and the room changed shape between labs. The two walls are always equal
     and always narrower than the floor — 1 : 1.6 : 1 — whether they are full
     or empty. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr) minmax(0, 1fr);

  /* STRETCH, not centre. Three panels sized to their own content stood at
     three different heights and three different vertical positions, so the
     room read as three loose cards rather than as three surfaces of one
     space — and it re-laid itself every time a proposition opened and the
     middle column grew. Full height always: the frame is the frame, and what
     changes inside a panel changes inside it. */
  align-items: stretch;
  gap: clamp(1.25rem, 3.4vw, 4.5rem);
  /* Symmetric block padding, and a perspective origin at the dead centre.
     A rotateY under perspective scales the panel about the perspective
     origin, so if the panel's own centre does not sit ON that origin the
     rotation displaces it VERTICALLY — which is how a wall ends up in the
     bottom corner. Line the two up and the rotation only ever turns. */
  /* Tight block padding on purpose: every pixel here comes off the height the
     three surfaces are allowed, and the surfaces are the room. */
  padding-block: var(--s-2);
  padding-inline: clamp(1.5rem, 4vw, 4.5rem);
  perspective: 1600px;
  perspective-origin: 50% 50%;
  min-block-size: 0;
}

/* The two walls. The rotation matches the room's own geometry, which is what
   makes the panel read as painted onto the wall rather than floating in front
   of it. transform-origin is the inner edge — the corner of the room. */
/* The panel IS the wall. Trying to align a transparent block of text to a
   painted surface behind it never quite lands — give the panel the surface and
   the two stop competing. */
.room__wall,
.room__floor {
  /* A FIXED size, not a stretched one and not a fitted one.
     Stretching them to the full row made the walls taller than they have ever
     been; fitting them to their content made all three different heights and
     re-laid the room whenever a card opened. So each surface is simply the
     size it is: its old maximum, always, with empty space at the bottom when
     there is less to say. Centred in the row, so the three sit on one axis. */
  align-self: center;
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-5) var(--s-5) var(--s-6);

  /* The walls are 80% of the screen, the floor 90% — see the floor's own rule.

     NO PERCENTAGES HERE, and that is the whole point of the second term being
     a calc() rather than the `89%` it was. A percentage block-size on a grid
     item resolves against the grid area, and on the first layout after the
     room is un-hidden that area is not yet definite — so the percentage falls
     back to auto and the panel sizes to its CONTENT. The room therefore opened
     short and only snapped to its real height once something forced a second
     layout, which is why it looked right after you clicked a proposition and
     wrong the moment it opened.
     --room-chrome is the bar plus this container's own block padding: what
     100svh has to give up before the surfaces get what is left. Everything
     here is now viewport units and one constant, which cannot be indefinite. */
  block-size: min(80svh, calc(100svh - var(--room-chrome)));
  overflow-y: auto;
  overscroll-behavior: contain;
  min-block-size: 0;
  min-inline-size: 0;
  background-color: color-mix(in srgb, var(--c-ground-up) 74%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  backdrop-filter: blur(var(--panel-blur)) saturate(0.92);
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-lab);
  backface-visibility: hidden;
}

/* The walls get more room inside than the floor does: their content is type
   set against a rotated edge, and type running to the edge of a panel that is
   itself turning away reads as clipped. The floor is a grid of cards, which
   already has its own gutters. */
.room__wall {
  padding-inline: clamp(1.25rem, 1.6vw, 2rem);
  padding-block: var(--s-5) var(--s-7);
}
.room__wall--left  { transform: rotateY(14deg);  transform-origin: 100% 50%; text-align: start; }
.room__wall--right { transform: rotateY(-14deg); transform-origin: 0% 50%; }

/* The floor is the one you are meant to act on, so it stands a little proud of
   the two that only describe: no rotation, a brighter ground, the accent rule
   doubled, and a taller box — it is the only surface with two sections in it. */
.room__floor {
  background-color: color-mix(in srgb, var(--c-ground-up) 86%, transparent);
  border-block-start-width: 3px;
  justify-items: center;

  /* Taller than the walls, deliberately. It is the surface you act on — the
     deck and the cases both live here — and standing a head above the two
     that only describe is what makes the arrangement read as a room with a
     front rather than three panels in a row. Same no-percentage rule as the
     walls; see the note there. */
  block-size: min(90svh, calc(100svh - var(--room-chrome)));
}

/* ---- the surface header. A number, a name, a line saying what it is for. */
.room__head {
  display: grid;
  gap: var(--s-1);
  inline-size: 100%;
  padding-block-end: var(--s-2);
  border-block-end: 1px solid var(--c-hair);
}
.room__head .room__eyebrow {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  color: var(--c-lab);
}
.room__n {
  font-family: var(--font-mono);
  color: var(--c-lab);
  opacity: 0.7;
}
.room__label { color: var(--c-lab); }
.room__sub { color: var(--c-text-4); }

.room__eyebrow { color: var(--c-lab); }
.room__subhead { color: var(--c-text-4); margin-block-start: var(--s-3); }

.room__title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.05rem + 1.4vw, 2.5rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 800;
  line-height: 1.02;
  color: var(--c-text);
  text-wrap: balance;
}
.room__title:focus-visible { outline-offset: 6px; }

.room__venue {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 600;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-md);
  color: var(--c-lab);
  line-height: 1.15;
  margin-block-start: var(--s-2);
}
/* The registered name. Present because it is real and sometimes needed on
   paper, subordinate because it is not what anybody calls the place. */
.room__formal { color: var(--c-text-4); margin-block-start: var(--s-1); }

.room__where { color: var(--c-text-4); }
.room__lead { color: var(--c-text-2); font-size: var(--fs-md); max-inline-size: 34ch; margin-block-start: var(--s-2); }

.room__figures {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: var(--s-1) var(--s-3);
  margin-block-start: var(--s-4);
  border-block-start: 1px solid var(--c-hair);
  padding-block-start: var(--s-3);
}
.room__figure {
  font-family: var(--font-mono);
  font-size: var(--fs-n2);
  font-weight: 500;
  line-height: 1;
  color: var(--c-lab);
  font-variant-numeric: tabular-nums;
}
.room__figures dd { color: var(--c-text-4); }

.room__registers { display: grid; gap: var(--s-1); }
.room__registers dt { color: var(--c-text-4); margin-block-start: var(--s-3); }
.room__tags { display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* Every chip on this wall is a button that opens a conversation about that
   subject, so it has to read as pressable. The "+N" used to be a <span>: it
   said "eleven more" and did nothing, which is the worst thing a control can
   do. It expands now, and looks like it will. */
.room__tags .chip--more { cursor: pointer; }
.room__tags .chip--more:hover { background-color: color-mix(in srgb, var(--c-lab) 22%, transparent); }
.room__partners { color: var(--c-text-3); font-size: var(--fs-sm); max-inline-size: none; }

.room__hint { color: var(--c-text-4); }

/* ---- cases: the same offer, after it happened.
   The deck above sells; this shows. Separated by a rule rather than a gap —
   it is the second half of one answer, not a new question. */
.room__cases {
  display: grid;
  gap: var(--s-3);
  inline-size: 100%;
  margin-block-start: var(--s-4);
  padding-block-start: var(--s-4);
  border-block-start: 1px solid var(--c-hair);
}
.room__cases-title { color: var(--c-lab); }
.room__cases-empty {
  color: var(--c-text-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-dense);
}

.cases { display: grid; gap: var(--s-2); inline-size: 100%; }

.case {
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-lab);
  background-color: color-mix(in srgb, var(--c-ground) 70%, transparent);
}

/* Closed, a case is a strip: thumbnail, who it was for, what was made. That is
   as much as fits on a floor this column already shares with the deck — and
   it is enough to decide whether to open it, which is the only job a closed
   card has. */
.case__face {
  inline-size: 100%;
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2);
  text-align: start;
  background-color: transparent;
  transition: background-color var(--t-2) var(--ease-rack);
}
.case__face:hover { background-color: color-mix(in srgb, var(--c-lab) 12%, transparent); }

.case__thumb {
  inline-size: 4.5rem;
  block-size: 3rem;
  object-fit: cover;
  display: block;
  background-color: var(--c-void);
}

.case__text { display: grid; gap: 2px; min-inline-size: 0; }
.case__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); }
.case__client { color: var(--c-lab); }
.case__when { color: var(--c-text-4); }

/* Says out loud that this is not a real case. It is the only thing standing
   between a layout placeholder and a published claim about a named
   organisation, so it is loud on purpose and it is on the closed face. */
.case__flag {
  padding: 1px var(--s-1);
  border: 1px solid var(--c-signal);
  color: var(--c-signal);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
}

.case__title {
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.25;
  color: var(--c-text);
  overflow-wrap: break-word;
}
.case__has-video { color: var(--c-text-4); }

/* Built once and collapsed, like the proposition cards: creating a <video> in
   the middle of the opening move costs a layout at the frame the viewer is
   watching hardest. */
.case__body-wrap {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  overflow: hidden;
  transition: grid-template-rows var(--t-5) var(--ease-land), opacity var(--t-3) var(--ease-rack);
}
.case__body-wrap > * { min-block-size: 0; }
.case__inner { display: grid; gap: var(--s-2); padding: 0 var(--s-3) var(--s-3); }

.case.is-open { border-color: var(--c-lab); }
.case.is-open .case__body-wrap { grid-template-rows: 1fr; opacity: 1; }
.case.is-open .case__face { background-color: color-mix(in srgb, var(--c-lab) 14%, transparent); }

.case__media { margin: 0; }
.case__media video,
.case__media img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background-color: var(--c-void);
}

.case__body { font-size: var(--fs-sm); line-height: var(--lh-dense); color: var(--c-text-2); }
.case__result { color: var(--c-lab); }

@media (prefers-reduced-motion: reduce) {
  .case__body-wrap { transition: none; }
}

/* The one action on the right wall, under the subjects it belongs to. */
.room__contact { justify-self: stretch; text-align: center; margin-block-start: var(--s-4); }

/* The deck's own hint goes when a card is forward — the instruction has been
   followed. The meet block stays: it is a different offer, and hiding it the
   moment somebody looks at a proposition is hiding the alternative exactly
   when they are deciding. */
.room__floor.is-open > .room__hint { opacity: 0; }

.deck {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  justify-items: stretch;
  gap: var(--s-3);
  inline-size: 100%;
  position: relative;
}

.card {
  position: relative;
  min-inline-size: 0;
  transition: transform var(--t-5) var(--ease-land), opacity var(--t-4) var(--ease-rack);
  transform-style: preserve-3d;
}

.card__face {
  inline-size: 100%;
  min-inline-size: 0;
  overflow-wrap: break-word;
  block-size: 100%;
  display: grid;
  align-content: start;
  gap: var(--s-2);
  padding: var(--s-4);
  text-align: start;
  background-color: color-mix(in srgb, var(--c-ground-up) 88%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur));
  backdrop-filter: blur(var(--panel-blur));
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-lab);
  min-block-size: 0;
  transition: background-color var(--t-2) var(--ease-rack), transform var(--t-3) var(--ease-land);
}
.card__face:hover { background-color: color-mix(in srgb, var(--c-lab) 14%, transparent); transform: translateY(-4px); }

.card__top { display: flex; justify-content: space-between; gap: var(--s-3); }
.card__num { color: var(--c-lab); }
.card__chain { color: var(--c-text-4); }

.card__title {
  min-inline-size: 0;
  font-size: var(--fs-md);
  font-weight: 500;
  line-height: 1.2;
  color: var(--c-text);
}
.card__venue { color: var(--c-text-4); }
.card__band { color: var(--c-lab); margin-block-start: var(--s-2); }

/* The explanation is in the DOM from the start, collapsed. Building it on open
   would cost a layout in the middle of the move, and the move is the point. */
.card__body {
  display: grid;
  gap: var(--s-3);
  grid-template-rows: 0fr;
  opacity: 0;
  overflow: hidden;
  transition: grid-template-rows var(--t-5) var(--ease-land), opacity var(--t-3) var(--ease-rack);
}
.card__body > * { min-block-size: 0; }
.card__inner { display: grid; gap: var(--s-3); }

/* The chosen card comes forward and the other two hold back. They are not
   hidden: the deck is still a deck, and you can see what you did not pick.

   .is-open is set by app.js; :has() is not in the selector at all any more.
   Firefox shipped :has() in 121 and Safari in 15.4, and everywhere older the
   deck never regrew its rows — the opened card landed on top of the two it was
   meant to push aside. Layout the interaction REQUIRES cannot ride on a
   selector some browsers do not have. */
.deck.is-open { grid-template-rows: auto auto; }

.card.is-forward {
  grid-column: 1 / -1;
  grid-row: 2;
  z-index: 2;
}
.card.is-forward .card__body {
  grid-template-rows: 1fr;
  opacity: 1;
  padding: var(--s-4);
  background-color: color-mix(in srgb, var(--c-ground-up) 92%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur));
  backdrop-filter: blur(var(--panel-blur));
  border: 1px solid var(--c-hair);
  border-block-start: none;
}
.card.is-forward .card__face { min-block-size: 0; }

.card.is-back {
  grid-row: 1;
  opacity: 0.42;
}
.card.is-back .card__face { padding: var(--s-2) var(--s-3); gap: var(--s-1); }
.card.is-back .card__title { font-size: var(--fs-sm); }
.card.is-back .card__venue,
.card.is-back .card__band { display: none; }
.card.is-back:hover { opacity: 0.85; }

.card__get { list-style: none; padding: 0; display: grid; gap: var(--s-2); }
.card__get li {
  position: relative;
  padding-inline-start: var(--s-4);
  font-size: var(--fs-sm);
  color: var(--c-text-2);
  line-height: var(--lh-dense);
}
.card__get li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.6em;
  inline-size: 6px;
  block-size: 1px;
  background-color: var(--c-lab);
}

.card__facts {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: var(--s-1) var(--s-3);
  font-size: var(--fs-sm);
}
.card__facts dt { color: var(--c-text-4); }
.card__facts dd { color: var(--c-text-2); }
.card__note { color: var(--c-text-4); }
.card__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }

/* Inside, the country's own furniture has nothing to point at — and the
   instrument strip is reading a map you are no longer looking at. */
.is-inside .rail,
.is-inside .register,
.is-inside .stage__type,
.is-inside .marks,
.is-inside .stage__instrument,
.is-inside .stage__back { opacity: 0; pointer-events: none; }

/* Below 1100px there is no room to stand in — three walls in a phone-width
   column is three walls you cannot read. The perspective comes off and the
   three surfaces become three TABS, which is the same document in the same
   order with only one of it on screen at a time.

   Tabs rather than a stack, because the stack was 2,289px tall on a 390px
   phone: everything past the first panel was below a fold with nothing to
   suggest it was there, and the exit was at the very bottom of it. */
@media (max-width: 1100px) {
  .room__surfaces {
    display: block;
    perspective: none;
    padding-block: var(--s-4) var(--s-7);
    padding-inline: var(--pad-x);
    overflow-y: auto;
    overscroll-behavior: contain;
    /* iOS keeps momentum scrolling on a nested box only if it is asked. */
    -webkit-overflow-scrolling: touch;
  }

  .room__tabs {
    display: flex;
    gap: var(--s-1);
    padding-inline: var(--pad-x);
    border-block-end: 1px solid var(--c-hair);
    background-color: color-mix(in srgb, var(--c-ground) 82%, transparent);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    position: relative;
    z-index: 2;
  }

  .room__wall,
  .room__floor {
    transform: none;

    /* block-size, not max-block-size. The desktop surfaces are a FIXED height
       so the three sit at one size whatever is in them; here there is only
       ever one of them on screen and the page itself scrolls, so a fixed
       height would either clip the content or leave a hole under it. `auto`
       is the undo — `max-block-size: none` alone leaves the fixed height in
       place, which is how the phone panel ended up 540px tall with its own
       content hanging out of it. */
    block-size: auto;
    max-block-size: none;
    overflow: visible;
    inline-size: 100%;
    padding: var(--s-4);
  }

  /* One surface at a time. `hidden` rather than opacity, so the two that are
     not showing cost no layout, take no tab stop and are skipped by a screen
     reader — a tab panel that is off screen but still focusable is a keyboard
     trap with extra steps. */
  .room__wall,
  .room__floor { display: none; }

  .room[data-surface="about"] .room__wall--left,
  .room[data-surface="offer"] .room__floor,
  .room[data-surface="here"]  .room__wall--right { display: grid; }

  .deck { grid-template-columns: 1fr; }
  .deck.is-open { grid-template-rows: none; }
  .card.is-forward, .card.is-back { grid-row: auto; grid-column: auto; }
  .card.is-back { opacity: 0.5; }
  .card.is-back .card__title { font-size: var(--fs-md); }

  .card__facts { grid-template-columns: 1fr; gap: 0; }
  .card__facts dt { margin-block-start: var(--s-2); }
}

/* Below 480px the worded exit and the cross say the same thing twice in a bar
   that has room for one of them. The arrow keeps the meaning. */
@media (max-width: 480px) {
  .room__out span:not(.room__out-arrow) { display: none; }
  .room__out { padding-inline: var(--s-3); }
}


/* ==========================================================================
   8. PROPOSITIONS
   ========================================================================== */

.props { display: grid; gap: var(--s-5); }

.prop {
  border-block-start: 1px solid var(--c-lab);
  padding-block-start: var(--s-4);
}

.prop__top { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); align-items: baseline; }
.prop__venue { color: var(--c-text-3); }
.prop__chain { color: var(--c-text-4); }

.prop__badge {
  margin-inline-start: auto;
  padding: 2px var(--s-2);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  border: 1px solid var(--c-hair);
  color: var(--c-text-3);
}
.prop__badge[data-booking="slot"] { color: var(--c-on-accent); background-color: var(--c-lab); border-color: var(--c-lab); }

.prop__title {
  font-family: var(--font-body);
  font-size: var(--fs-lg);
  font-weight: 500;
  color: var(--c-text);
  margin-block: var(--s-2) var(--s-3);
  line-height: 1.28;
}

.prop__get { list-style: none; padding: 0; display: grid; gap: var(--s-2); }
.prop__get li {
  position: relative;
  padding-inline-start: var(--s-4);
  font-size: var(--fs-sm);
  color: var(--c-text-2);
  line-height: var(--lh-dense);
}
.prop__get li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.6em;
  inline-size: 6px;
  block-size: 1px;
  background-color: var(--c-lab);
}

.prop__facts {
  margin-block-start: var(--s-4);
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: var(--s-1) var(--s-3);
  font-size: var(--fs-sm);
}
.prop__facts dt { color: var(--c-text-4); }
.prop__facts dd { color: var(--c-text-2); }

.prop__price { display: grid; gap: 2px; }
.prop__band { color: var(--c-text); font-weight: 500; }
.prop__note { color: var(--c-text-4); }

.prop__tags { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-block-start: var(--s-3); max-inline-size: none; }


/* ==========================================================================
   9. BOOKING
   ========================================================================== */

.booking {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--s-5);
  background-color: color-mix(in srgb, var(--c-void) 82%, transparent);
}

.booking__panel {
  inline-size: min(38rem, 100%);
  max-block-size: 90svh;
  overflow-y: auto;
  padding: var(--s-6);
  background-color: var(--c-ground-up);
  border: 1px solid var(--c-hair);
  border-block-start: 3px solid var(--c-lab);
}

.booking__head { position: relative; padding-inline-end: var(--s-7); }
.booking__title {
  font-family: var(--font-display);
  font-size: var(--fs-d4);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 800;
  line-height: var(--lh-d4);
  margin-block: var(--s-2);
}
.booking__lead { color: var(--c-text-3); font-size: var(--fs-sm); max-inline-size: none; }

.booking__close {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  inline-size: var(--target-min);
  block-size: var(--target-min);
  color: var(--c-text-3);
  border: 1px solid var(--c-hair);
}

.booking__form { display: grid; gap: var(--s-4); margin-block-start: var(--s-5); }
.booking__row { display: grid; gap: var(--s-2); border: none; padding: 0; margin: 0; }
.booking__row legend { padding: 0; }
.booking__chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.booking__input {
  inline-size: 100%;
  padding: var(--s-2) var(--s-3);
  min-block-size: var(--target-min);
  background-color: var(--c-ground-lift);
  border: 1px solid var(--c-hair);
  color: var(--c-text);
  font-size: var(--fs-md);
}
.booking__input:focus-visible { border-color: var(--c-signal); }
.booking__input[aria-invalid="true"] { border-color: var(--c-alert); }

.booking__error { color: var(--c-alert); min-block-size: 1rem; }
.booking__error:empty { min-block-size: 0; }

.booking__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-block-start: var(--s-2); }
.booking__done { display: grid; gap: var(--s-4); justify-items: start; }


/* ==========================================================================
   10. BUTTONS AND CHIPS
   ========================================================================== */

.button {
  padding: var(--s-3) var(--s-5);
  min-block-size: var(--target-min);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: var(--track-body);
  border: 1px solid var(--c-lab);
  color: var(--c-lab);
  transition: background-color var(--t-2) var(--ease-rack), color var(--t-2) var(--ease-rack);
}
.button:hover { background-color: var(--c-lab); color: var(--c-on-accent); }

.button--primary { background-color: var(--c-lab); color: var(--c-on-accent); }
.button--primary:hover { background-color: var(--c-signal); border-color: var(--c-signal); }

.button--ghost { border-color: var(--c-hair); color: var(--c-text-3); }
.button--ghost:hover { background-color: var(--c-ground-lift); color: var(--c-text); }

.button--book { margin-block-start: var(--s-4); }

.chip {
  display: inline-block;
  padding: 2px var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-mono);
  color: var(--c-text-3);
  border: 1px solid var(--c-hair);
}
.chip--sm { color: var(--c-text-4); }
.chip--more { color: var(--c-lab); border-color: var(--c-lab); }

.chip--choice {
  cursor: pointer;
  padding: var(--s-2) var(--s-3);
  min-block-size: var(--target-min);
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-sm);
  font-family: var(--font-body);
  color: var(--c-text-2);
}
.chip--choice:hover { border-color: var(--c-lab); color: var(--c-text); }
input:checked + .chip--choice { background-color: var(--c-lab); color: var(--c-on-accent); border-color: var(--c-lab); }
input:focus-visible + .chip--choice { outline: 2px solid var(--c-focus-core); outline-offset: 2px; box-shadow: 0 0 0 4px var(--c-focus-halo); }


/* ==========================================================================
   11. FALLBACKS
   With no WebGL the drawing stays and the page is complete: every lab, every
   proposition, and the whole booking path.
   ========================================================================== */

.fallback, .no-webgl .stage__load { color: var(--c-signal); }
.no-webgl .stage__load { opacity: 1; }
.no-webgl .stage__load p { display: none; }

.fallback { padding: var(--s-7) var(--pad-x); }
.fallback a { color: var(--c-signal); text-decoration: underline; }

body.is-booking { overflow: hidden; }


/* ==========================================================================
   12. RESPONSIVE
   On a phone the country still stands, but the register becomes the primary
   control: there is no hover on a touch screen, so selection has to be a
   thing you can swipe to.
   ========================================================================== */

@media (max-width: 1200px) {
  .dossier { inline-size: min(30rem, 56vw); }
  .register { inline-size: min(22rem, 42vw); }
  .stage__type { inline-size: min(40ch, 52vw); }
}

@media (max-width: 900px) {
  /* Taller than the viewport is wrong here: the stage is the first screen and
     it holds three things that all have to be visible at once — the claim, the
     country, and the way in. 100svh minus the bar is exactly the room there
     is, and everything below is sized to fit inside it rather than to look
     right and overflow. */
  .stage { block-size: 100svh; min-block-size: 32rem; }

  .stage__type {
    inset-block-start: calc(var(--nav-h) + 2svh);
    inset-block-end: auto;
    transform: none;
    inline-size: min(92vw, 30ch);
  }

  /* The claim was three lines at 4rem on a 390px screen and took half the
     first screen with it, which is half a screen the map did not get. */
  .stage__type .title-card { font-size: clamp(1.75rem, 1.1rem + 3.4vw, 2.75rem); }
  .stage__sub { font-size: var(--fs-md); max-inline-size: 32ch; }

  .is-focused .stage__type { transform: translateY(1rem); }

  /* The instrument strip reads out the pointer position and the focal length.
     There is no pointer on a phone, and it was sitting in the same corner as
     the lab list — two things in one place, one of which nobody can use. */
  .stage__instrument { display: none; }

  .register {
    inset-block-start: auto;
    inset-block-end: var(--s-6);
    inset-inline: 0;
    inline-size: auto;
    padding-inline: var(--pad-x);
  }
  .register__title, .register__hint { display: none; }
  .register__list {
    display: flex;
    grid-template-columns: none;
    gap: var(--s-2);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    border-block-start: none;
    padding-block-end: var(--s-2);
    grid-template-columns: none;
  }
  .register__item { flex: 0 0 14rem; scroll-snap-align: start; }
  .register__item + .register__item .register__button { border-inline-start: 1px solid var(--c-hair); }
  .register__button {
    block-size: 100%;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--s-1);
    padding: var(--s-3) var(--s-4) var(--s-4);
    border: 1px solid var(--c-hair);
    border-block-start: 2px solid transparent;
    background-color: color-mix(in srgb, var(--c-ground-up) 82%, transparent);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
  }
  /* One column on a phone: the two-column head collides the venue line with
     the proposition count as soon as either wraps. */
  .register__num,
  .register__name,
  .register__place,
  .register__count { grid-column: 1; grid-row: auto; justify-self: start; }

  .dossier {
    inset-block-start: auto;
    inline-size: 100%;
    block-size: 72svh;
    border-inline-start: none;
  }

  .stage__instrument { font-size: 0.6875rem; }
  .stage__instrument span:nth-child(2) { display: none; }
}

@media (max-width: 560px) {
  .prop__facts { grid-template-columns: 1fr; gap: 0 0; }
  .prop__facts dt { margin-block-start: var(--s-2); }
  .booking__panel { padding: var(--s-5); }
  .night-nav__link { display: none; }
}


/* ==========================================================================
   13. THE PLATES
   The daylight sheet's argument, in the dark. Same words — every string is a
   key from the shared deck — and the only thing that changes is the light.

   The daylight version is ink on stock and carries its structure in RULES.
   That does not survive the move: hairlines on a near-black ground read as
   dirt. So the structure moves into SPACE and into one accent line per plate,
   and the type does the rest. Wider measures, more air, fewer edges.
   ========================================================================== */

.plates {
  position: relative;
  scroll-margin-block-start: var(--nav-h);
  z-index: var(--z-content);
  background-color: var(--c-ground);
  border-block-start: 1px solid var(--c-hair);
}

.plate {
  position: relative;
  z-index: 1;
  max-inline-size: var(--w-field);
  margin-inline: auto;
  padding: clamp(4rem, 9svh, 8rem) var(--pad-x);
  display: grid;
  gap: var(--s-5);
  border-block-end: 1px solid var(--c-hair);
}

.plate:last-child { border-block-end: none; }

/* Everything after the chapters needs its own ground.
   .chapters holds a sticky full-height canvas whose stage carries
   `margin-block-end: -100svh` — the trick that lets five screens of type
   scroll over one object. The section's box therefore ends 100svh before the
   drawing does, and the plate that follows is laid out over the tail of it.
   With a transparent plate that is a building drawn through a price list.

   The ground is put on the plates that follow rather than taken off the
   chapters, because the overlap IS the effect: the last chapter is meant to
   run under the next section and be covered by it, not to stop early. */
.chapters ~ .plate { background-color: var(--c-ground); }

.plate__eyebrow { color: var(--c-signal); }

/* One display element per plate, and it is the claim. Everything under it is
   body, at two sizes. */
.plate__claim {
  font-family: var(--font-display);
  font-size: clamp(2rem, 1.2rem + 2.4vw, 3.75rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 900;
  font-weight: 900;
  line-height: 0.98;
  letter-spacing: var(--track-d1);
  color: var(--c-text);
  text-wrap: balance;
  max-inline-size: 20ch;
}

.plate__lead {
  font-size: var(--fs-xl);
  line-height: var(--lh-lead);
  color: var(--c-text-2);
  max-inline-size: 46ch;
}
.plate__lead--wide { max-inline-size: 62ch; }

.plate__body { color: var(--c-text-3); max-inline-size: 52ch; line-height: var(--lh-body); }
.plate__body--wide { max-inline-size: 66ch; }

.plate__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr));
  gap: var(--s-5) var(--s-7);
}

/* The turn in the argument. It gets the one accent rule on the plate, because
   it is the sentence the whole page pivots on. */
.plate__turn {
  font-family: var(--font-display);
  font-size: var(--fs-d4);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 700;
  line-height: var(--lh-d4);
  color: var(--c-text);
  max-inline-size: 30ch;
  padding-inline-start: var(--s-5);
  border-inline-start: 2px solid var(--c-signal);
}

.consequence {
  display: grid;
  gap: var(--s-3);
  padding-block: var(--s-2);
}
.consequence__list { list-style: none; padding: 0; display: grid; gap: var(--s-2); }
.consequence__list li {
  position: relative;
  padding-inline-start: var(--s-5);
  color: var(--c-text-2);
  font-size: var(--fs-lg);
}
.consequence__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.72em;
  inline-size: 12px;
  block-size: 1px;
  background-color: var(--c-signal-hot);
}

/* --- the six verbs. A job list, laid out as one. --- */
/* --- what the network does. Six jobs, not six paragraphs.

   The copy calls itself a job list rather than a mission statement, so it is
   set as one: the VERB is the headline and the largest thing in each card, the
   number is a big quiet numeral behind it, and the explanation is the smallest
   thing rather than the bulk. Read the six verbs across in five seconds and
   you have the whole section; read one card and you have the detail. A grid of
   six equal paragraphs gives you neither. --- */
.verbs {
  list-style: none;
  padding: 0;
  display: grid;

  /* Three, two, one — a divisor of six at every step, never auto-fit.
     auto-fit resolved to four columns on a wide window, which left two empty
     cells in the second row; with the hairline showing through the gap those
     empties are two grey rectangles at the end of the list that look like
     content failed to load. Six items want a grid that divides six. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  margin-block-start: var(--s-5);

  /* One hairline between cards, drawn by the gap over a ruled ground rather
     than by a border per card — six bordered boxes double every internal
     line and the grid reads as a table of cells. */
  background-color: var(--c-hair);
  border: 1px solid var(--c-hair);
}

.verb {
  position: relative;
  display: grid;
  gap: var(--s-2);
  align-content: start;
  padding: var(--s-5) var(--s-5) var(--s-6);
  background-color: var(--c-ground);
  overflow: hidden;
  transition: background-color var(--t-2) var(--ease-rack);
}
.verb:hover { background-color: var(--c-ground-up); }

/* The numeral is scenery, not information — the order is in the reading, and
   the count is in the heading. Big, dim, and clipped by the card. */
.verb__n {
  position: absolute;
  inset-block-start: var(--s-3);
  inset-inline-end: var(--s-4);
  font-family: var(--font-mono);
  font-size: clamp(2.5rem, 1.5rem + 3vw, 4rem);
  line-height: 1;
  color: var(--c-signal);
  opacity: 0.16;
  pointer-events: none;
  transition: opacity var(--t-3) var(--ease-rack);
}
.verb:hover .verb__n { opacity: 0.3; }

.verb__title {
  position: relative;
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 800;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: clamp(1.25rem, 0.9rem + 1.1vw, 1.75rem);
  color: var(--c-text);
  line-height: 1.05;
  padding-block-end: var(--s-3);
}

/* A short rule under the verb, in the accent — the same device the plate
   headings use, so the section belongs to the page it is on. */
.verb__title::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 2rem;
  block-size: 2px;
  background-color: var(--c-signal);
  transition: inline-size var(--t-3) var(--ease-land);
}
.verb:hover .verb__title::after { inline-size: 3.5rem; }

.verb__body,
.verb p {
  color: var(--c-text-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-dense);
  max-inline-size: none;
}

@media (max-width: 1000px) { .verbs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .verbs { grid-template-columns: 1fr; } }

@media (prefers-reduced-motion: reduce) {
  .verb__title::after { transition: none; }
}

/* --- the chain. Four steps and the line they sit on. --- */
.chain {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 0;
  margin-block: var(--s-4);
  border-block-start: 1px solid var(--c-signal);
}
.chain__step {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-4) var(--s-4) 0;
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 600;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-md);
  color: var(--c-text);
}
.chain__step .instrument { color: var(--c-signal); }

/* --- five ambitions, said out loud. --- */
.ambitions {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
  gap: 0;
  margin-block-start: var(--s-3);
}
.ambitions dt {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-lg);
  color: var(--c-signal);
  padding-block: var(--s-4);
  border-block-start: 1px solid var(--c-hair);
}
.ambitions dd {
  color: var(--c-text-2);
  padding-block: var(--s-4);
  padding-inline-start: var(--s-5);
  border-block-start: 1px solid var(--c-hair);
  max-inline-size: 56ch;
}

/* --- 2030. --- */
.impact {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--s-6);
  margin-block-start: var(--s-4);
}
.impact__col { display: grid; gap: var(--s-3); align-content: start; }
.impact__title {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-md);
  color: var(--c-text);
  padding-block-end: var(--s-2);
  border-block-end: 1px solid var(--c-hair);
}
.impact__col p { color: var(--c-text-3); max-inline-size: none; }
.impact__list { list-style: none; padding: 0; display: grid; gap: var(--s-3); }

/* Each societal outcome is carried by the lab that delivers it, so the five
   colours appear here and nowhere else on the plates. */
.impact__list li {
  position: relative;
  padding-inline-start: var(--s-5);
  color: var(--c-text-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-dense);
}
.impact__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.55em;
  inline-size: 10px;
  block-size: 2px;
  background-color: var(--c-lab);
}

/* --- the close. --- */
.plate--close { padding-block: clamp(5rem, 12svh, 10rem); }
.plate__close {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 1.4rem + 3.4vw, 5rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 900;
  font-weight: 900;
  line-height: 0.94;
  letter-spacing: var(--track-d1);
  color: var(--c-text);
  text-wrap: balance;
  max-inline-size: 16ch;
}
.plate__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-block-start: var(--s-3); max-inline-size: none; }
.plate__actions .button { --c-lab: var(--c-signal); text-decoration: none; }

/* Inside a lab the page behind must not scroll away underneath the room. */
body.is-inside { overflow: hidden; }

@media (max-width: 760px) {
  .ambitions { grid-template-columns: 1fr; }
  .ambitions dd { padding-inline-start: 0; border-block-start: none; padding-block-start: 0; }
  .chain { border-block-start: none; }
  .chain__step { border-block-start: 1px solid var(--c-signal); }
}


/* ==========================================================================
   14. THE FILM BANDS, THE CANDOUR, THE FIVE
   The daylight sheet's photographic surface, brought over. js/film.js is
   reused as-is; only the styling is rewritten, because css/site.css grades
   every frame for a LIGHT ground and the same filter on black turns a lit
   room into grey soup.

   The grade here is the reverse: hold the blacks, pull the saturation down
   toward the page's single hue, and let the frame's own light be the brightest
   thing in the section. The scrim runs upward from the bottom so the caption
   always has something to sit on whatever the footage does.
   ========================================================================== */

.plate--film { padding: 0; border-block-end: 1px solid var(--c-hair); max-inline-size: none; }

.film {
  position: relative;
  margin: 0;
  overflow: hidden;
  background-color: var(--c-void);
}

.film__frame {
  position: relative;
  overflow: hidden;
  background-color: var(--c-void);
}
.film--tall .film__frame { block-size: clamp(24rem, 58vw, 42rem); }

.film__media {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
  /* Graded for a dark page: desaturated toward the sheet's own hue, contrast
     up, brightness down, so a lit studio does not punch a hole in the night. */
  filter: saturate(0.62) contrast(1.14) brightness(0.72);
}

/* The scrim. Always there, always bottom-up, so a caption never depends on
   what the footage happens to be doing behind it. */
.film__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--c-void) 55%, transparent) 0%, transparent 34%),
    linear-gradient(0deg, var(--c-void) 0%, color-mix(in srgb, var(--c-void) 72%, transparent) 26%, transparent 62%);
}

/* Grain over the frame, at the same strength as the rest of the page, so
   footage and render belong to one image. */
.film__frame::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--grade-grain);
  mix-blend-mode: overlay;
  background-image: var(--media-grain-image);
  background-size: 180px 180px;
}

/* A band whose media 404s falls back to the drawn plate rather than a hole —
   generated photography lands in img/ after the markup does. */
.film[data-media="drawn"] .film__frame {
  block-size: clamp(9rem, 18vw, 14rem);
  background-image:
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--c-lab, var(--c-signal)) 20%, transparent) 0 1px,
      transparent 1px 14px);
}
.film[data-media="drawn"] .film__media { display: none; }

.film__cap {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  z-index: 2;
  display: grid;
  gap: var(--s-2);
  max-inline-size: var(--w-field);
  margin-inline: auto;
  padding: var(--s-6) var(--pad-x) var(--s-7);
}

.film__eyebrow { color: var(--c-signal); }

.film__line {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 1.1rem + 2vw, 3.25rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 900;
  line-height: 0.98;
  letter-spacing: var(--track-d1);
  color: var(--c-text);
  max-inline-size: 22ch;
}
.film__line--2 { color: var(--c-signal); }
.film__meta { color: var(--c-text-3); }

/* --- the candour ---------------------------------------------------------
   The one place the page says what it does NOT cover. It gets a panel rather
   than a rule, because on a dark ground an admission set in the same body
   style as everything else reads as filler. */
.plate--honest .candour {
  margin-block-start: var(--s-4);
  padding: var(--s-5);
  background-color: color-mix(in srgb, var(--c-ground-up) 70%, transparent);
  border-inline-start: 2px solid var(--c-signal-hot);
  max-inline-size: 62ch;
  display: grid;
  gap: var(--s-2);
}
.candour__title {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: var(--fs-md);
  color: var(--c-text);
}
.plate--honest .candour p { color: var(--c-text-2); max-inline-size: none; }

/* --- the five ------------------------------------------------------------ */
/* The five, and what is happening around them, on one plate.
   News and the agenda used to be a plate of their own further down. They are
   not a separate subject — what a lab announced and what it is running next
   month is the same information as what the lab IS, only dated — so they sit
   beside the five and read as a feed of the thing on their left. */
.labsgrid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 20rem);
  align-items: start;
  gap: var(--s-6) var(--s-7);
  margin-block-start: var(--s-4);
}

.labbands {
  display: grid;

  /* TWO per row, fixed. auto-fit gave three on a wide window and the fifth lab
     then sat alone on a row of its own looking like an afterthought; two rows
     of two and one centred pair is a shape, three-and-two is a leftover. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-6) var(--s-5);
}

/* The feed. A column, hairline-separated from the labs, scrolling with the
   page rather than stuck — it is reference, not navigation. */
.labsfeed {
  display: grid;
  gap: var(--s-6);
  align-content: start;
  padding-inline-start: var(--s-5);
  border-inline-start: 1px solid var(--c-hair);
}
.labsfeed__col { display: grid; gap: var(--s-3); align-content: start; }
.labsfeed__title {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  font-size: var(--fs-lg);
  line-height: 1.05;
  color: var(--c-text);
  text-wrap: balance;
}

/* Below 1100 the feed goes under the labs and the border becomes a rule across
   the top — a left border on a full-width block is a stray vertical line. */
@media (max-width: 1100px) {
  .labsgrid { grid-template-columns: 1fr; }
  .labsfeed {
    padding-inline-start: 0;
    border-inline-start: none;
    padding-block-start: var(--s-5);
    border-block-start: 1px solid var(--c-hair);
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    gap: var(--s-6) var(--s-7);
  }
}

@media (max-width: 700px) {
  .labbands { grid-template-columns: 1fr; }
}

.labband { display: grid; gap: var(--s-3); align-content: start; }

.labband__head {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  padding-block-end: var(--s-2);
  border-block-end: 1px solid var(--c-lab);
  color: var(--c-text-4);
}

.labband .film--strip .film__frame { block-size: clamp(8rem, 14vw, 11rem); }

.labband__name {
  font-family: var(--font-display);
  font-size: var(--fs-d4);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 800;
  line-height: 1;
}
.labband__name a { color: var(--c-lab); text-decoration: none; }
.labband__name a:hover { text-decoration: underline; text-underline-offset: 0.18em; }

.labband__lead { color: var(--c-text); font-size: var(--fs-md); line-height: 1.3; max-inline-size: none; }
.labband__line { color: var(--c-text-3); font-size: var(--fs-sm); line-height: var(--lh-dense); max-inline-size: none; }

@media (prefers-reduced-motion: reduce) {
  .film__media { animation: none !important; }
}

/* An inset frame inside a plate of type: the argument cuts to picture without
   leaving the column. It carries a lab accent because it is a frame OF one. */
.film--inset {
  max-inline-size: 44rem;
  border-block-start: 2px solid var(--c-lab);
}
.film--inset .film__frame { block-size: clamp(12rem, 26vw, 20rem); }

/* The second full-bleed band. Shorter than the opener — a reprise, not a
   repeat. */
.film--wide .film__frame { block-size: clamp(18rem, 40vw, 30rem); }


/* ==========================================================================
   15. THE FINDER
   It filters the register, so it sits on the register, and it uses the same
   grammar: one hairline, one accent, no box.
   ========================================================================== */

.rail {
  position: absolute;
  inset-block-start: calc(var(--nav-h) + 4svh);
  inset-inline-end: var(--pad-x);
  z-index: var(--z-content);
  inline-size: min(21rem, 26vw);
  display: grid;
  gap: var(--s-6);
  max-block-size: calc(100svh - var(--nav-h) - 12svh);
}

.finder { display: grid; gap: var(--s-3); }

.finder__title { color: var(--c-text-4); text-align: end; }

.finder__input {
  inline-size: 100%;
  min-block-size: var(--target-min);
  padding: var(--s-2) var(--s-3);
  background-color: color-mix(in srgb, var(--c-ground-up) 76%, transparent);
  -webkit-backdrop-filter: blur(var(--panel-blur));
  backdrop-filter: blur(var(--panel-blur));
  border: 1px solid var(--c-hair);
  border-block-end-color: var(--c-rule);
  color: var(--c-text);
  font-size: var(--fs-sm);
}
.finder__input::placeholder { color: var(--c-text-4); }
.finder__input:focus-visible { border-color: var(--c-signal); }

.finder__intents { display: flex; flex-wrap: wrap; gap: var(--s-1); justify-content: flex-end; }
.finder__intents .chip--choice { font-size: var(--fs-2xs); padding: var(--s-1) var(--s-2); min-block-size: 2rem; }
.finder__intents [aria-pressed="true"] {
  background-color: var(--c-signal);
  color: var(--c-on-accent);
  border-color: var(--c-signal);
}

.finder__count { text-align: end; color: var(--c-signal); min-block-size: 1rem; }

.register { position: static; inline-size: auto; }

/* Filtered out, not removed. A search that empties the screen takes the map
   away from someone who was only curious. */
.register__item.is-out { opacity: 0.28; }
.register__item.is-out .register__button { border-inline-start-color: transparent; }
.mark.is-out { opacity: 0.25; }

.register__why {
  grid-column: 1 / -1;
  color: var(--c-signal);
  padding-block-start: var(--s-1);
}

@media (max-width: 1100px) {
  /* The rail is a right-hand column on a desktop and a FOOTER on a phone: the
     map needs the full width to be readable at all, so search and labs sit
     under it rather than beside it. Anchored to the bottom of the stage rather
     than dropped into flow, because the canvas and the .marks overlay are both
     inset:0 of .stage — taking the rail out of that box would leave the map
     labels projected against a frame that is no longer where the map is. */
  .rail {
    position: absolute;
    inset-block-start: auto;
    inset-block-end: 0;
    inset-inline: 0;
    inline-size: auto;
    max-block-size: none;
    gap: var(--s-2);
    padding-inline: var(--pad-x);
    padding-block: var(--s-3) var(--s-4);

    /* A GROUND, not a transparent overlay. The rail sits on the map here
       rather than beside it, and white type over a 3D drawing of a country at
       night is only legible by accident — it depends on what happens to be
       behind each letter. A graded ground fixes that without a hard edge:
       the map fades into the panel instead of being cut off by it. */
    background-image: linear-gradient(to bottom,
      transparent 0%,
      color-mix(in srgb, var(--c-void) 72%, transparent) 22%,
      color-mix(in srgb, var(--c-void) 94%, transparent) 55%,
      var(--c-void) 100%);
  }

  /* THE mobile bug: .rail is a grid, and an auto/1fr track never shrinks below
     its content's min-content width. The lab list is a horizontal scroller of
     14rem cards, so the track resolved to 1184px inside a 390px phone — the
     search box and the whole list were laid out off-screen and clipped away by
     the stage's overflow:hidden. A grid child needs min-inline-size:0 before it
     is allowed to be narrower than what is inside it. */
  .rail > * { min-inline-size: 0; }

  .finder__title, .finder__count { text-align: start; }
  .finder__intents { justify-content: flex-start; }
  .register { inset-block-start: auto; }
}

/* The rail is a footer here, and every pixel it takes is a pixel of country.
   At its first size it was 358px of an 844px screen — with the claim above it
   that left the map 156px to be a map in, and the two southernmost labs were
   drawn underneath the rail rather than on the land.

   So the footer is compressed rather than the map being cropped: the chips
   become one scrolling row instead of two wrapped ones, the section titles go
   (the search field's own placeholder already says what it is), and the lab
   cards lose the lines that repeat what the card above already said. */
@media (max-width: 1100px) {
  .rail { gap: var(--s-2); }

  .finder { gap: var(--s-2); }
  .finder__title { display: none; }

  .finder__input { min-block-size: var(--target-min); padding-block: var(--s-1); }

  /* One row that scrolls, not two that wrap. Wrapping was costing a whole
     40px line for one chip. */
  .finder__intents {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding-block-end: 2px;
  }
  .finder__intents::-webkit-scrollbar { display: none; }
  .finder__intents .chip--choice { flex: none; }

  .finder__count { min-block-size: 0; }
  .finder__count:empty { display: none; }

  .register__item { flex: 0 0 11.5rem; }
  .register__button { padding: var(--s-2) var(--s-3) var(--s-3); gap: 2px; }
  .register__place { display: none; }
}


/* ==========================================================================
   16. THE PROPOSITION PLATE
   A drawn picture per card, generated from the proposition. Four patterns,
   one per position in the chain, rotated and pitched by a hash of the id — so
   the fifteen cards are fifteen pictures, and all fifteen are one family.

   Real photography, when it exists, comes in as an <img> and the pattern
   steps aside. Until then this is honestly a drawing, which is the point: a
   fabricated photograph of a real lab would be a lie about a real place.
   ========================================================================== */

.card__plate {
  position: relative;
  margin: 0 0 var(--s-3);
  block-size: clamp(4.5rem, 9vw, 7rem);
  overflow: hidden;
  background-color: var(--c-void);
  border-block-end: 1px solid var(--c-lab);
}

.card__plate img { inline-size: 100%; block-size: 100%; object-fit: cover; filter: saturate(0.62) contrast(1.14) brightness(0.72); }

.card__plate::after {
  content: "";
  position: absolute;
  inset: -40%;
  transform: rotate(var(--angle, 24deg));
}

/* EXPERIMENT — scattered points. Nothing is settled yet. */
.card__plate[data-pattern="dots"]::after {
  background-image: radial-gradient(circle at 50% 50%, var(--c-lab) 0.9px, transparent 1px);
  background-size: var(--pitch, 10px) var(--pitch, 10px);
  opacity: 0.55;
}

/* PROTOTYPE — a grid. Something is being set out. */
.card__plate[data-pattern="grid"]::after {
  background-image:
    linear-gradient(0deg, var(--c-lab) 1px, transparent 1px),
    linear-gradient(90deg, var(--c-lab) 1px, transparent 1px);
  background-size: var(--pitch, 10px) var(--pitch, 10px);
  opacity: 0.32;
}

/* TEST / VALIDATION — hatching. It is being gone over. */
.card__plate[data-pattern="hatch"]::after {
  background-image: repeating-linear-gradient(45deg,
    var(--c-lab) 0 1px, transparent 1px var(--pitch, 10px));
  opacity: 0.40;
}

/* DEMONSTRATE — bars. It is being shown. */
.card__plate[data-pattern="bars"]::after {
  background-image: repeating-linear-gradient(90deg,
    var(--c-lab) 0 2px, transparent 2px calc(var(--pitch, 10px) * 1.6));
  opacity: 0.42;
}

/* IMPLEMENTATION — a solid field with one clean edge. It is built. */
.card__plate[data-pattern="solid"]::after {
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--c-lab) 55%, transparent) 0%,
    transparent 78%);
  opacity: 0.7;
}

.card__plate[data-pattern="photo"]::after { content: none; }

/* On the card that has come forward the plate gets more room, because there
   is more room. */
.card.is-forward .card__plate { block-size: clamp(6rem, 12vw, 9rem); }
.card.is-back .card__plate { display: none; }


/* ==========================================================================
   17. THE CHAPTERS
   Five sections over one sticky canvas. The type scrolls; the object does not
   move down the page, it turns and assembles in place. That contrast — a
   still object and moving words — is what makes a scroll feel authored rather
   than merely long.
   ========================================================================== */

.chapters {
  position: relative;
  background-color: var(--c-ground);
  border-block-start: 1px solid var(--c-hair);

  /* The sticky canvas is z-index 1 inside here, and the plates that follow
     are unpositioned — so without a stacking context of its own the canvas
     paints straight over the next section and its text disappears behind a
     building. isolation makes .chapters the ceiling for everything inside it. */
  isolation: isolate;
}

.chapters__head {
  max-inline-size: var(--w-field);
  margin-inline: auto;
  padding: clamp(4rem, 9svh, 8rem) var(--pad-x) 0;
  display: grid;
  gap: var(--s-4);
  position: relative;
  z-index: 2;
}

/* The canvas is sticky, full-height, and sits BEHIND the type. It is one
   element for all five chapters — five canvases would be five contexts. */
.chapters__stage {
  position: sticky;
  inset-block-start: 0;
  block-size: 100svh;
  margin-block-end: -100svh;
  pointer-events: none;
  z-index: 1;
}
/* The stage the frames are drawn into. Sized by its sticky parent; the rule
   further down gives it position:relative so the canvas can be inset:0 in it. */

.chapter {
  position: relative;
  z-index: 2;
  min-block-size: 190svh;
  display: grid;
  align-content: center;
  max-inline-size: var(--w-field);
  margin-inline: auto;
  padding-inline: var(--pad-x);
  pointer-events: none;
}

/* The type sits in the left third and stays there; the object owns the
   centre-right. On a narrow screen the object goes behind the type instead,
   which is why the panel has a ground. */
.chapter__type {
  position: sticky;
  inset-block-start: 34svh;
  inline-size: min(28rem, 38vw);
  display: grid;
  gap: var(--s-3);
  justify-items: start;
  pointer-events: auto;
  opacity: 0.25;
  transform: translateY(1.5rem);
  transition: opacity var(--t-5) var(--ease-rack), transform var(--t-5) var(--ease-land);
}
.chapter.is-on .chapter__type { opacity: 1; transform: none; }

.chapter__n { color: var(--c-lab); }

.chapter__name {
  font-family: var(--font-display);
  font-size: clamp(3rem, 1.8rem + 4vw, 6.5rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 900;
  font-weight: 900;
  line-height: 0.9;
  letter-spacing: var(--track-d1);
  color: var(--c-text);
}

.chapter__lead { font-size: var(--fs-xl); line-height: var(--lh-lead); color: var(--c-text); max-inline-size: 24ch; }
.chapter__line { color: var(--c-text-3); font-size: var(--fs-sm); line-height: var(--lh-dense); max-inline-size: 34ch; }
.chapter__where { color: var(--c-text-4); }
.chapter__go { margin-block-start: var(--s-3); --c-lab: var(--c-lab); text-decoration: none; }

/* With no WebGL the chapters are five blocks of type about five labs, which
   is the part that carries the information. */
.chapters.is-flat .chapters__stage { display: none; }
.chapters.is-flat .chapter { min-block-size: 0; padding-block: var(--s-7); }
.chapters.is-flat .chapter__type { position: static; opacity: 1; transform: none; inline-size: auto; }

@media (max-width: 900px) {
  .chapter { min-block-size: 150svh; }
  .chapter__type {
    inline-size: auto;
    inset-block-start: 46svh;
    padding: var(--s-4);
    background-color: color-mix(in srgb, var(--c-ground) 78%, transparent);
    -webkit-backdrop-filter: blur(var(--panel-blur));
    backdrop-filter: blur(var(--panel-blur));
    border-block-start: 2px solid var(--c-lab);
  }
}

@media (prefers-reduced-motion: reduce) {
  .chapter { min-block-size: 0; padding-block: var(--s-7); }
  .chapter__type { position: static; opacity: 1; transform: none; }
  .chapters__stage { position: static; block-size: 60svh; margin-block-end: 0; }
}


/* ==========================================================================
   18. NEWS AND THE AGENDA
   Two lists, one plate. News reads backwards and is prose; the agenda reads
   forwards and is a date block plus a place — because you read one and you
   travel to the other, and the layouts should not pretend otherwise.

   A lab-scoped entry takes that lab's colour, so both lists can be read by
   colour before they are read by date.
   ========================================================================== */

/* .plate--agenda / .agenda / .agenda__col are GONE from index.html — the two
   lists moved into the labs plate as .labsfeed. The rules below still ship
   because night.html has not been restructured and still uses them; everything
   from .newslist down is shared by both and is what actually styles the items.
   Delete these three when night.html follows. */
.plate--agenda { padding-block: clamp(3.5rem, 7svh, 6rem); }

.agenda {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  gap: var(--s-7) var(--s-8);
}

.agenda__col { display: grid; gap: var(--s-3); align-content: start; }

.agenda__title {
  font-family: var(--font-display);
  font-size: var(--fs-d4);
  font-variation-settings: wdth var(--wdth-display), wght 800;
  line-height: var(--lh-d4);
  color: var(--c-text);
  margin-block-end: var(--s-2);
}

/* --- news --------------------------------------------------------------- */
.newslist { list-style: none; padding: 0; display: grid; gap: 0; }

.newsitem {
  display: grid;
  gap: var(--s-2);
  padding-block: var(--s-4);
  border-block-start: 1px solid var(--c-hair);
}
.newsitem:first-child { border-block-start-color: var(--c-lab); }

.newsitem__head { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.newsitem__date { color: var(--c-text-4); }
.newsitem__scope { color: var(--c-lab); }

.newsitem__title {
  font-family: var(--font-body);
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: 1.25;
  color: var(--c-text);
}
.newsitem__body { color: var(--c-text-3); font-size: var(--fs-sm); line-height: var(--lh-dense); max-inline-size: 52ch; }

/* --- the agenda --------------------------------------------------------- */
.eventlist { list-style: none; padding: 0; display: grid; gap: 0; }

.eventitem {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--s-4);
  padding-block: var(--s-4);
  border-block-start: 1px solid var(--c-hair);
}
.eventitem:first-child { border-block-start-color: var(--c-lab); }

/* The date block. A big number and a short month is how a diary reads at a
   glance; a full date in prose is how a diary reads slowly. */
.eventitem__when {
  display: grid;
  justify-items: start;
  align-content: start;
  gap: 2px;
  max-inline-size: none;
}
.eventitem__day {
  font-family: var(--font-mono);
  font-size: var(--fs-n2);
  font-weight: 500;
  line-height: 0.9;
  color: var(--c-lab);
  font-variant-numeric: tabular-nums;
}
.eventitem__month { color: var(--c-text-4); text-transform: uppercase; letter-spacing: var(--track-eyebrow); }

/* An event with no date says so, in the space the date would have taken.
   Nothing is more suspicious on an agenda than a date that turns out to be a
   guess. */
.eventitem__tbc { color: var(--c-text-4); line-height: 1.2; }

.eventitem__body { display: grid; gap: var(--s-1); align-content: start; }
.eventitem__scope { color: var(--c-lab); }

.eventitem__title {
  font-family: var(--font-body);
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: 1.25;
  color: var(--c-text);
}
.eventitem__where { color: var(--c-text-4); }

@media (max-width: 560px) {
  .eventitem { grid-template-columns: 1fr; gap: var(--s-2); }
  .eventitem__when { display: flex; gap: var(--s-2); align-items: baseline; }
  .eventitem__day { font-size: var(--fs-xl); }
}


/* ==========================================================================
   19. THE INTRODUCTION
   The one third-party frame on the page. It is given a ground and a rule so
   it reads as part of the sheet rather than as a window cut into it, and the
   aspect ratio is held by the wrapper so the embed can be any width without
   the page having to know its height.
   ========================================================================== */

.plate--intro { padding-block-end: clamp(3rem, 6svh, 5rem); }

.intro { margin: var(--s-4) 0 0; max-inline-size: 60rem; }

.intro__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  background-color: var(--c-void);
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-signal);
  overflow: hidden;
}

.intro__frame iframe {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: block;
  border: 0;
}

/* ==========================================================================
   19. THE SCROLL FRAMES
   A camera push-in through a photograph of a real lab, scrubbed by the scroll.
   It replaced a gaussian capture that cost about 15 MB a lab; this is 48 webp
   frames at about 3.4 MB, fetched only when the chapters come near.

   The room's walls have NO backdrop of their own any more — not a capture and
   not a sequence. The three.js interior behind them is what the room is.
   ========================================================================== */

.chapters__canvas { position: relative; inline-size: 100%; block-size: 100%; }

.frames__canvas {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: block;
  /* Inert: there is nothing to click on a backdrop, and text selection and
     scrolling over it should behave like the rest of the page. */
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-5) var(--ease-rack);
  background-color: var(--c-void);
}

/* Only once a sequence is actually behind this chapter. Fading in from nothing
   hides the first frame arriving, which would otherwise appear as a cut. */
.chapters--has-frames .frames__canvas { opacity: 0.78; }

/* THE SCRIM, and it is not decoration.
   The captures this replaced were dark rooms, so type sat on them unaided. A
   photograph of a lit green-screen studio is the opposite — measured against
   it, the chapter's lead and its city line were pale grey on bright green and
   effectively unreadable. So the ground is put back under the type: heaviest
   on the left third where the type actually is, fading to nothing across the
   frame so the picture is still a picture. */
.chapters__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-5) var(--ease-rack);
  background-image:
    linear-gradient(to right,
      var(--c-void) 0%,
      color-mix(in srgb, var(--c-void) 88%, transparent) 26%,
      color-mix(in srgb, var(--c-void) 45%, transparent) 52%,
      color-mix(in srgb, var(--c-void) 20%, transparent) 100%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--c-void) 55%, transparent) 0%,
      transparent 30%,
      transparent 68%,
      color-mix(in srgb, var(--c-void) 60%, transparent) 100%);
}
.chapters--has-frames .chapters__stage::after { opacity: 1; }

/* On a narrow screen the type is a panel across the whole width rather than a
   left column, so a left-weighted scrim would darken the wrong side. */
@media (max-width: 900px) {
  .chapters__stage::after {
    background-image: linear-gradient(to bottom,
      color-mix(in srgb, var(--c-void) 70%, transparent) 0%,
      color-mix(in srgb, var(--c-void) 40%, transparent) 40%,
      color-mix(in srgb, var(--c-void) 78%, transparent) 100%);
  }
}

@media (max-width: 1100px) {
  /* The type sits directly on the picture here with far less room to move out
     of the way. */
  .chapters--has-frames .frames__canvas { opacity: 0.6; }
}

@media (prefers-reduced-motion: reduce) {
  .frames__canvas { transition: none; }
}


/* ==========================================================================
   20. WHEN THERE IS NO GLASS
   Every panel on this sheet is a translucent ground plus a blur, and the two
   are one decision: the ground is only that transparent BECAUSE the blur is
   flattening what is behind it. Where the blur is missing — Firefox with the
   pref off, iOS before 15.4, any browser with compositing disabled — the
   translucency alone leaves white type over a moving 3D map.

   So the ground goes opaque wherever the blur cannot land. Tested for both
   spellings, because Safari answered `no` to the unprefixed name until 18
   while supporting the effect the whole time.
   ========================================================================== */

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .room__bar,
  .room__tabs { background-color: var(--c-ground); }

  .room__wall,
  .room__floor,
  .card__face,
  .card.is-forward .card__body { background-color: var(--c-ground-up); }

  .room__out,
  .room__cross,
  .stage__back,
  .finder__input,
  .register__button,
  .chapter__type { background-color: var(--c-ground); }
}




/* ==========================================================================
   22. THE FACILITIES EXPLORER
   The plate below this lists the offer lab by lab. This is the same data
   entered from the other side: a visitor who knows a technique, a domain or a
   city, and does not yet know which of five labs to ask.

   Two halves that drive each other — a radial graph you point at, and blocks
   that answer. Neither is decoration: every node is a button and every tag on
   a block moves the graph.
   ========================================================================== */

.explore-host { margin-block-start: var(--s-6); }

.explore {
  display: grid;
  gap: var(--s-5);
  padding: var(--s-5);
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-signal);
  background-color: color-mix(in srgb, var(--c-ground-up) 60%, transparent);
}

.explore__bar { display: grid; gap: var(--s-3); }

.explore__input {
  inline-size: 100%;
  min-block-size: var(--target-min);
  padding: var(--s-2) var(--s-3);
  background-color: color-mix(in srgb, var(--c-ground) 82%, transparent);
  border: 1px solid var(--c-hair);
  border-block-end-color: var(--c-rule);
  color: var(--c-text);
  font-size: var(--fs-md);
}
.explore__input::placeholder { color: var(--c-text-4); }
.explore__input:focus-visible { border-color: var(--c-signal); }

.explore__lenses { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.explore__lenses [aria-pressed="true"] {
  background-color: var(--c-signal);
  color: var(--c-on-accent);
  border-color: var(--c-signal);
}

/* What is currently filtering, as things you can take off again. A filter you
   cannot see is a filter you cannot undo. */
.explore__active { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1); }
.explore__active[hidden] { display: none; }

.chip--active {
  background-color: color-mix(in srgb, var(--c-signal) 20%, transparent);
  border-color: var(--c-signal);
  color: var(--c-text);
  cursor: pointer;
}
.chip--active:hover { background-color: color-mix(in srgb, var(--c-signal) 34%, transparent); }

.explore__clear {
  background: none;
  border: none;
  color: var(--c-text-4);
  text-decoration: underline;
  cursor: pointer;
  padding-inline: var(--s-2);
}
.explore__clear:hover { color: var(--c-signal); }

.explore__count { color: var(--c-signal); min-block-size: 1rem; }

.explore__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
}

/* The sound switch. Two bars that grow into four when it is on — a speaker
   glyph at this size is a smudge, and the state has to read at a glance from
   across a nav bar. */
.topnav__sound {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--target-min);
  block-size: var(--target-min);
  padding: 0;
  background: none;
  border: 1px solid var(--c-hair);
  color: var(--c-text-3);
  cursor: pointer;
}
.topnav__sound[hidden] { display: none; }
.topnav__sound:hover { border-color: var(--c-signal); color: var(--c-signal); }
.topnav__sound:focus-visible { outline: 2px solid var(--c-signal); outline-offset: 2px; }

.topnav__sound-icon {
  inline-size: 14px;
  block-size: 14px;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 50%, 4px 50%, 8px 50%, 12px 50%;
  background-size: 2px 30%, 2px 55%, 2px 30%, 2px 12%;
  transition: background-size var(--t-2) var(--ease-rack);
}
/* ON IS FILLED, NOT MERELY COLOURED.
   Hover already lifts the border and the glyph to the signal colour, so an
   "on" state that only changed those two was indistinguishable from a hovered
   "off" — and the pointer is ON the button at exactly the moment you most need
   to read what just happened. A solid block cannot be confused with an
   outline, whatever the pointer is doing. */
.topnav__sound.is-on {
  background-color: var(--c-signal);
  border-color: var(--c-signal);
  color: var(--c-on-accent);
}
.topnav__sound.is-on:hover {
  background-color: var(--c-signal);
  color: var(--c-on-accent);
}
.topnav__sound.is-on .topnav__sound-icon {
  background-size: 2px 45%, 2px 100%, 2px 70%, 2px 35%;
}

@media (prefers-reduced-motion: reduce) {
  .topnav__sound-icon { transition: none; }
}

/* ---- the graph ---- */
.explore__graph { margin: 0; display: grid; gap: var(--s-2); position: sticky; inset-block-start: calc(var(--nav-h) + var(--s-3)); }
.explore__hint { color: var(--c-text-4); line-height: var(--lh-dense); }

.graph { inline-size: 100%; block-size: auto; display: block; overflow: visible; }

/* A spoke is a fact — this lab has this technique.

   These used to be 1px of rule grey at 0.30 opacity, on the theory that eighty
   of them at full strength is a solid disc. True, and it made the figure read
   as BROKEN: a term owned by one lab drew one hairline nobody could see, so the
   ring looked full of words that connected to nothing. The lines were always
   there — the count has never disagreed with the data — they were just below
   the threshold of the screen.

   They now carry their lab's colour, which does two jobs at once: it lifts a
   single spoke clear of the ground, and it says where that spoke goes without
   the eye having to follow it back to the middle. */
.graph__edge {
  fill: none;
  stroke: var(--c-lab, var(--c-rule));
  stroke-width: 1.4;
  stroke-linecap: round;
  opacity: 0.55;
  transition: opacity var(--t-2) var(--ease-rack), stroke-width var(--t-2) var(--ease-rack);
}

.graph__edge[data-lab="ix3"]   { --c-lab: var(--c-ix3); }
.graph__edge[data-lab="mix"]   { --c-lab: var(--c-mix); }
.graph__edge[data-lab="prime"] { --c-lab: var(--c-prime); }
.graph__edge[data-lab="brix"]  { --c-lab: var(--c-brix); }
.graph__edge[data-lab="shine"] { --c-lab: var(--c-shine); }

.graph__edge.is-on { opacity: 1; stroke-width: 2.4; }
.graph__edge.is-dim { opacity: 0.10; }

/* Pointing at a word, or at a lab. `is-shy` is what makes one term legible in
   a figure of eighty arcs; without it colour alone still leaves you tracing. */
.graph__edge.is-lit { opacity: 1; stroke-width: 2.4; }
.graph__edge.is-shy { opacity: 0.06; }

.graph__term { cursor: pointer; }
.graph__dot {
  fill: var(--c-ground-up);
  stroke: var(--c-rule);
  stroke-width: 1.5;
  transition: fill var(--t-2) var(--ease-rack), stroke var(--t-2) var(--ease-rack);
}
.graph__label {
  font-family: var(--font-mono);
  font-size: 13px;
  fill: var(--c-text-3);
  transition: fill var(--t-2) var(--ease-rack);
}
.graph__term:hover .graph__dot,
.graph__term:focus-visible .graph__dot { fill: var(--c-signal); stroke: var(--c-signal); }
.graph__term:hover .graph__label,
.graph__term:focus-visible .graph__label { fill: var(--c-text); }
.graph__term.is-on .graph__dot { fill: var(--c-signal); stroke: var(--c-signal); }
.graph__term.is-on .graph__label { fill: var(--c-signal); }
.graph__term.is-dim { opacity: 0.35; }

/* The five sit in the middle and never move, whatever is filtered. A ring that
   rearranges itself is a ring you have to re-read every time. */
.graph__lab { cursor: pointer; }
.graph__labdot {
  fill: color-mix(in srgb, var(--c-lab) 26%, var(--c-ground));
  stroke: var(--c-lab);
  stroke-width: 2;
  transition: fill var(--t-2) var(--ease-rack), opacity var(--t-2) var(--ease-rack);
}
.graph__lablabel {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.04em;
  fill: var(--c-text);
}
.graph__lab:hover .graph__labdot,
.graph__lab:focus-visible .graph__labdot { fill: color-mix(in srgb, var(--c-lab) 55%, var(--c-ground)); }
.graph__lab.is-on .graph__labdot { fill: var(--c-lab); }
.graph__lab.is-dim { opacity: 0.28; }

.graph__lab[data-lab="ix3"]   { --c-lab: var(--c-ix3); }
.graph__lab[data-lab="mix"]   { --c-lab: var(--c-mix); }
.graph__lab[data-lab="prime"] { --c-lab: var(--c-prime); }
.graph__lab[data-lab="brix"]  { --c-lab: var(--c-brix); }
.graph__lab[data-lab="shine"] { --c-lab: var(--c-shine); }

/* ---- the blocks ----

   THE COLUMN SCROLLS; THE SECTION DOES NOT GROW.

   Every proposition rendered at once made this plate as tall as its longest
   list — four screens of cards beside a figure that ended after one, and taller
   again with every package added. The graph is the point of the section and it
   was stranded at the top of it.

   --explore-graph-h is measured from the figure by js/night/explorer.js on
   every render and on resize. The fallback matters: with no JS height set, this
   is a plain unbounded column, which is the correct thing to degrade to. */
.explore__results {
  display: grid;
  gap: var(--s-3);
  align-content: start;
  max-block-size: var(--explore-graph-h, none);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-inline-end: var(--s-2);
}
.explore__none { color: var(--c-text-3); font-size: var(--fs-sm); line-height: var(--lh-dense); }

/* Narrow enough to read as a rail, not a second scrollbar on the page. */
.explore__results::-webkit-scrollbar { inline-size: 8px; }
.explore__results::-webkit-scrollbar-track { background-color: transparent; }
.explore__results::-webkit-scrollbar-thumb {
  background-color: color-mix(in srgb, var(--c-rule) 60%, transparent);
  border-radius: 4px;
}
.explore__results { scrollbar-width: thin; scrollbar-color: var(--c-rule) transparent; }

/* Which of the two the column is answering with. */
.explore__shows { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.explore__shows [aria-pressed="true"] {
  background-color: var(--c-signal);
  color: var(--c-on-accent);
  border-color: var(--c-signal);
}

.eblock {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--c-hair);
  border-inline-start: 3px solid var(--c-lab);
  background-color: color-mix(in srgb, var(--c-ground) 74%, transparent);
  transition: border-color var(--t-2) var(--ease-rack), background-color var(--t-2) var(--ease-rack);
}
.eblock:hover { background-color: color-mix(in srgb, var(--c-ground-up) 82%, transparent); }

.eblock[data-lab="ix3"]   { --c-lab: var(--c-ix3); }
.eblock[data-lab="mix"]   { --c-lab: var(--c-mix); }
.eblock[data-lab="prime"] { --c-lab: var(--c-prime); }
.eblock[data-lab="brix"]  { --c-lab: var(--c-brix); }
.eblock[data-lab="shine"] { --c-lab: var(--c-shine); }

/* A service has no lab of its own — several hold it — so it gets the rule
   colour down its edge rather than one lab's, and no price band anywhere. */
.eblock--service { --c-lab: var(--c-rule); }
.eblock__note { color: var(--c-text-4); line-height: var(--lh-dense); }
.eblock__who { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1); }
.eblock__who-title { color: var(--c-text-4); margin-inline-end: var(--s-1); }

.eblock__head { display: flex; justify-content: space-between; gap: var(--s-3); }
.eblock__lab { color: var(--c-lab); }
.eblock__chain { color: var(--c-text-4); }
.eblock__title { font-size: var(--fs-md); font-weight: 500; line-height: 1.2; color: var(--c-text); }
.eblock__venue { color: var(--c-text-4); }
.eblock__tags { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.eblock__band { color: var(--c-lab); }
.eblock__actions { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-block-start: var(--s-1); }

@media (max-width: 1100px) {
  .explore__body { grid-template-columns: 1fr; }
  .explore__graph { position: static; }

  /* Stacked, the figure is above the list rather than beside it, so there is
     no height to match — and a scroll area nested inside the page scroll on a
     phone is a trap that eats the gesture. The list is cut short in JS instead;
     see listLimit() in js/night/explorer.js. */
  .explore__results { max-block-size: none; overflow-y: visible; padding-inline-end: 0; }
  .explore__more { justify-self: start; }
}

@media (prefers-reduced-motion: reduce) {
  .graph__edge, .graph__dot, .graph__label, .graph__labdot { transition: none; }
}

/* Whose lab it is. Subordinate to the venue and the city — it is provenance,
   not an address — and only some labs have one. */
.room__by,
.labband__by { color: var(--c-lab); }
.room__by { margin-block-start: var(--s-1); }
.labband__by { margin-block-start: calc(var(--s-1) * -1); }

/* A chip that OPENS something, as against the ones that only label. The room's
   right wall is made of these: every register is a subject you can ask about,
   so each one has to read as pressable. Lost when the facilities listing was
   deleted — it was defined next to it, but the room is the thing that uses it. */
.chip--service {
  min-block-size: 2rem;
  cursor: pointer;
  transition: background-color var(--t-2) var(--ease-rack),
              border-color var(--t-2) var(--ease-rack),
              color var(--t-2) var(--ease-rack);
}
.chip--service:hover,
.chip--service:focus-visible {
  background-color: color-mix(in srgb, var(--c-lab) 22%, transparent);
  border-color: var(--c-lab);
  color: var(--c-text);
}


/* ==========================================================================
   23. THE CHAIN, WALKED
   Four stages that were four words in a row. They are the spine of the whole
   proposition — every lab runs the same line — but as static type they said
   only that the line exists, never that anything was on it.

   Each stage is a button now, carrying its own tally before it is pressed: a
   row of four identical buttons gives no reason to press any of them.
   ========================================================================== */

.chain-host { display: grid; gap: var(--s-3); margin-block: var(--s-5) var(--s-4); }

.chainx {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background-color: var(--c-hair);
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-signal);
}

.chainx__face {
  inline-size: 100%;
  block-size: 100%;
  display: grid;
  align-content: start;
  gap: var(--s-2);
  padding: var(--s-4);
  text-align: start;
  background-color: var(--c-ground);
  transition: background-color var(--t-2) var(--ease-rack);
}
.chainx__face:hover { background-color: var(--c-ground-up); }
.chainx__face.is-open { background-color: color-mix(in srgb, var(--c-signal) 16%, var(--c-ground)); }

.chainx__stepn { color: var(--c-signal); }
.chainx__steptitle {
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  font-size: var(--fs-md);
  line-height: 1.1;
  color: var(--c-text);
}
.chainx__steptally { color: var(--c-text-4); }
.chainx__face.is-open .chainx__steptally { color: var(--c-signal); }

.chainx__hint { color: var(--c-text-4); }

/* ---- what is at the stage you pressed */
.chainx__detail {
  display: grid;
  gap: var(--s-4);
  padding: var(--s-5);
  border: 1px solid var(--c-hair);
  border-inline-start: 3px solid var(--c-signal);
  background-color: color-mix(in srgb, var(--c-ground-up) 70%, transparent);
}
.chainx__detail[hidden] { display: none; }

.chainx__head { display: grid; gap: var(--s-1); }
.chainx__n { color: var(--c-signal); }
.chainx__title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 800;
  line-height: 1.05;
  color: var(--c-text);
}
.chainx__count { color: var(--c-signal); }

.chainx__props {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: var(--s-2);
}

.chainx__prop[data-lab="ix3"]   { --c-lab: var(--c-ix3); }
.chainx__prop[data-lab="mix"]   { --c-lab: var(--c-mix); }
.chainx__prop[data-lab="prime"] { --c-lab: var(--c-prime); }
.chainx__prop[data-lab="brix"]  { --c-lab: var(--c-brix); }
.chainx__prop[data-lab="shine"] { --c-lab: var(--c-shine); }

.chainx__propface {
  inline-size: 100%;
  display: grid;
  gap: 2px;
  padding: var(--s-3);
  text-align: start;
  border: 1px solid var(--c-hair);
  border-inline-start: 2px solid var(--c-lab);
  background-color: color-mix(in srgb, var(--c-ground) 72%, transparent);
  transition: background-color var(--t-2) var(--ease-rack);
}
.chainx__propface:hover { background-color: color-mix(in srgb, var(--c-lab) 14%, transparent); }
.chainx__lab { color: var(--c-lab); }
.chainx__name { font-size: var(--fs-sm); line-height: 1.25; color: var(--c-text); }

.chainx__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding-block-start: var(--s-3);
  border-block-start: 1px solid var(--c-hair);
}
.chainx__labs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1); }
.chainx__labs-title { color: var(--c-text-4); margin-inline-end: var(--s-1); }

.chainx__labs .chip[data-lab="ix3"]   { --c-lab: var(--c-ix3); }
.chainx__labs .chip[data-lab="mix"]   { --c-lab: var(--c-mix); }
.chainx__labs .chip[data-lab="prime"] { --c-lab: var(--c-prime); }
.chainx__labs .chip[data-lab="brix"]  { --c-lab: var(--c-brix); }
.chainx__labs .chip[data-lab="shine"] { --c-lab: var(--c-shine); }

@media (max-width: 900px) {
  .chainx { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .chainx { grid-template-columns: 1fr; }
  .chainx__actions { flex-direction: column; align-items: stretch; }
}


/* ==========================================================================
   24. WHY NOW — the circle, and the cut
   The three consequences were a bulleted list, and a list of three says these
   are three separate problems. They are one: talent leaving is why
   organisations fall back, which is why clients stay hesitant, which is why
   talent leaves. The whole argument for a national network is that it cuts
   that loop — so the loop is drawn, and then visibly cut.
   ========================================================================== */

.plate--why { gap: var(--s-6); }

.why__setup {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr));
  gap: var(--s-4) var(--s-7);
}
.why__gap {
  font-size: var(--fs-md);
  line-height: var(--lh-lead);
  color: var(--c-text-2);
  padding-inline-start: var(--s-4);
  border-inline-start: 2px solid var(--c-signal);
}

.why__loop { display: grid; gap: var(--s-4); justify-items: center; }
.why__label { color: var(--c-signal); justify-self: start; }

/* A FLAT loop, not a circle. A true circle at a readable card size is nearly
   800px tall and almost all of it is the hole in the middle — the diagram was
   mostly empty floor. An ellipse half as tall says the same thing about the
   direction of travel in the space three cards actually need. */
.why__ring {
  position: relative;
  inline-size: min(58rem, 100%);
  aspect-ratio: 100 / 62;
  display: grid;
  place-items: center;
}

/* The arcs sit behind the three points and carry the arrowheads: a loop is
   only an argument if you can see which way round it runs. */
.why__arcs {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  color: var(--c-signal);
  pointer-events: none;
}
.why__arc {
  fill: none;
  stroke: currentColor;
  stroke-width: 0.7;
  opacity: 0.55;
}

/* The middle of the loop, which is otherwise a large hole. aria-hidden: the
   three points below already say what this is, and a screen reader does not
   need the caption of a picture it is not being shown. */
.why__centre {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-annot), "wght" 700;
  text-transform: uppercase;
  letter-spacing: var(--track-annot);
  font-size: clamp(1rem, 0.8rem + 0.8vw, 1.5rem);
  color: var(--c-signal);
  opacity: 0.5;
  white-space: nowrap;
  pointer-events: none;
}

.why__points {
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  inset: 0;
}

.why__point {
  position: absolute;
  inline-size: min(21rem, 34%);
  display: grid;
  gap: var(--s-1);
  padding: var(--s-4);
  background-color: color-mix(in srgb, var(--c-ground-up) 88%, transparent);
  border: 1px solid var(--c-hair);
  border-block-start: 2px solid var(--c-signal);
  font-size: var(--fs-sm);
  line-height: var(--lh-dense);
  color: var(--c-text);
}
.why__n { color: var(--c-signal); }

/* Twelve, four and eight o'clock — the three corners of the loop. */
.why__point--a { inset-block-start: 0; inset-inline-start: 50%; transform: translateX(-50%); }
.why__point--b { inset-block-end: 0; inset-inline-end: 0; }
.why__point--c { inset-block-end: 0; inset-inline-start: 0; }

/* Where the loop is cut. It is the answer to everything above it, so it gets
   the accent ground the consequences do not. */
.why__break {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-5);
  background-color: color-mix(in srgb, var(--c-signal) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-signal) 40%, transparent);
  border-inline-start: 3px solid var(--c-signal);
}
.why__turn {
  font-family: var(--font-display);
  font-size: clamp(1.375rem, 1rem + 1.4vw, 2rem);
  font-variation-settings: "wdth" var(--wdth-display), "wght" 800;
  line-height: 1.1;
  color: var(--c-text);
  text-wrap: balance;
}
.why__answer { font-size: var(--fs-md); line-height: var(--lh-lead); color: var(--c-text-2); max-inline-size: 62ch; }

/* Below 760 the ring stops being a ring: three cards at 42% width on a phone
   are three cards nobody can read, and a circle small enough to fit is a
   circle with no room inside it. The order is the argument, so it becomes a
   column with the hand-offs drawn as marks between the cards. */
@media (max-width: 760px) {
  .why__ring { aspect-ratio: auto; display: block; }
  .why__centre { display: none; }
  .why__arcs { display: none; }
  .why__points { position: static; display: grid; gap: var(--s-5); }
  .why__point {
    position: static;
    inline-size: 100%;
    transform: none;
  }
  .why__point + .why__point { position: relative; }
  .why__point + .why__point::before {
    content: "↓";
    position: absolute;
    inset-block-start: calc(var(--s-5) * -1);
    inset-inline-start: var(--s-4);
    block-size: var(--s-5);
    display: grid;
    place-items: center;
    color: var(--c-signal);
  }
}
