/* ============================================================
   SCS.2 — shared site theme
   Transit wayfinding look, after Vignelli's subway signage: Helvetica,
   black station bars with white type, coloured route bullets and
   stripes on aged paper, square corners, hard offset shadows.
   ============================================================ */

:root {
  color-scheme: light;
  --page-bg: #f2efe8;
  --card-bg: #fcfbf7;
  --surface: #fcfbf7;
  --text: #111111;
  --muted: #62615c;
  --border: #d9d5c9;
  --border-strong: #8e8a7e;
  --accent: #00933c;
  --accent-dark: #00752f;
  /* The accent as a GROUND, with --on-accent type reversed out of it.
     Not the same value as --accent, which is the route colour: white
     on #00933c is 4.01:1, under the 4.5 the rest of the site's type is
     held to, so a button label was the one piece of type on the site
     that did not meet its own standard. The darker green carries white
     at 5.86:1 and still reads as a block on the paper (5.10). The dots,
     the stripe and every other use of green are untouched. */
  --accent-solid: #00752f;
  --accent-soft: #e8f2e8;
  --accent-soft-border: #b5d6bc;
  --on-accent: #ffffff;
  --notice-bg: #fdf6e0;
  --notice-border: #e5cd8a;
  --danger-bg: #fdf0ef;
  --danger-border: #eeb4ae;
  /* Red as words on the page, rather than as a route colour. The line
     red is right on paper but sits too dark and too saturated on the
     dark ground, where it buzzes against the black; dark mode lifts it
     to a warmer coral that carries the same meaning and stays legible. */
  --alert-ink: #ee352e;
  /* Warm light for something asking to be looked at: a flat translucent
     amber laid inside the box, not a shadow around it, so the hard
     square edge the rest of the site keeps stays exactly where it is.
     Flat rather than pooled — a gradient made the box read as unevenly
     lit; what should vary is how strong it is, not where. */
  --glow-fill: rgba(226, 176, 60, 0.4);
  --glow-edge: #e2c179;
  --mono: "Roboto Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; /* quest codes */
  --chrome-fade: 500ms cubic-bezier(0.16, 1, 0.3, 1); /* scroll-linked bars: fast start, long tail */
  --chrome-slide: 450ms cubic-bezier(0.16, 1, 0.3, 1);
  --shadow: 3px 3px 0 rgba(17, 17, 17, 0.14);
  --shadow-hover: 3px 3px 0 rgba(17, 17, 17, 0.28);
  /* Signage: the black objects keep their ink in both themes. A hero's
     sign bar, a plate, the quest sheet, the submit board and a picked
     chip are drawings, not furniture, and they read as the same object
     whichever theme the page is in. */
  --sign-bg: #131313;
  --sign-ink: #ffffff;
  --sign-muted: rgba(255, 255, 255, 0.72);
  /* The chrome is the other thing, and it does follow the theme: the
     top bar, the footer, the app's bottom bar and the lightbox's bar.
     It sits a shade off the page rather than on it, and a hairline is
     what separates the two — the five-route stripe that used to run
     along every one of those edges is gone. The stripe itself is not:
     it is still the mark on the paper things (the ticket, the
     confirmation email, the posters), where it is the only colour and
     has a page of its own to sit at the edge of. */
  --bar-bg: var(--surface);
  --bar-ink: var(--text);
  --bar-muted: var(--muted);
  --bar-edge: #cdc8ba;
  /* The five routes light the app's bottom bar, and they were drawn for
     black signage: on the light bar the yellow is 1.6:1, under the 3:1
     a solid glyph needs. It takes the lift --alert-ink takes in the
     other direction — the line yellow on the dark bar, a darker gold on
     the light one. The other four carry on both. */
  --tab-yellow: #8a6a00;
  /* the route palette (MTA line colours) — same in the dark theme,
     they were drawn for black signage */
  --line-red: #ee352e;
  --line-orange: #ff6319;
  --line-yellow: #fccc0a;
  --line-green: #00933c;
  --line-blue: #0039a6;
  --line-lime: #6cbe45;
  --line-purple: #b933ad;
  --line-brown: #996633;
  --line-grey: #a7a9ac;
  --line-sky: #00a1de;
  /* Five routes running in parallel: the coloured edge of every bar.
     The stops are whole pixels, not fifths. At 8px tall a fifth is
     1.6px, which no screen can draw: each line landed on a different
     pixel boundary, the browser part-covered the ones between, and the
     five came out at four different weights with the blue reading
     separate from the rest. Every bar carrying this is 10px, 2px a
     colour, with nothing between them. Separating the five with a pixel
     of the sign's own black took the stripe to 14px, and at that height
     on every band edge it read as an object in its own right rather
     than as a rule. */
  --stripe-h: 10px;
  /* the one gutter every band on the page is built on */
  --gutter: 26px;
  /* ---- the type ladder ----
     Six stops with real distance between them, and nothing between the
     stops. A signage system has a handful of sizes that each mean
     something; this page had grown about eighteen, most of them within
     a fifth of each other, which is a CSS scale rather than a sign
     scale and reads as mush. */
  --t-display: clamp(2.05rem, 5.4vw, 4.2rem);
  --t-head: clamp(1.5rem, 3.1vw, 2.1rem);
  --t-lede: clamp(1.06rem, 1.5vw, 1.22rem);
  --t-body: 1rem;
  --t-small: 0.88rem;
  --t-micro: 0.72rem;
  --stripe: linear-gradient(to bottom,
    var(--line-red) 0 2px, var(--line-orange) 2px 4px,
    var(--line-yellow) 4px 6px, var(--line-green) 6px 8px,
    var(--line-blue) 8px 10px);
}

html[data-theme="dark"] {
  color-scheme: dark;
  --page-bg: #121212;
  --card-bg: #1b1b19;
  --surface: #1b1b19;
  --text: #f0eee7;
  --muted: #a5a39a;
  --border: #34332e;
  --border-strong: #4c4a43;
  --accent: #2fb063;
  --accent-dark: #55c581;
  /* On the dark ground the existing pairing already clears it —
     #00933c under #0a130e is 4.71:1 — and the darker green would go
     the wrong way, so this theme keeps the route colour as its ground. */
  --accent-solid: #00933c;
  --accent-soft: #142a1b;
  --accent-soft-border: #26513a;
  --on-accent: #0a130e;
  --notice-bg: #272113;
  --notice-border: #574a28;
  --danger-bg: #2a1716;
  --danger-border: #5b302d;
  --alert-ink: #ff6b60;
  --glow-fill: rgba(255, 198, 92, 0.22);
  --glow-edge: #6a5730;
  --shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
  --shadow-hover: 3px 3px 0 rgba(47, 176, 99, 0.3);
  --sign-bg: #0c0c0c;
  --sign-ink: #ffffff;
  --sign-muted: rgba(255, 255, 255, 0.72);
  --bar-edge: #3c3b35;
  --tab-yellow: var(--line-yellow);
}

/* ---------------- Motion tokens (transitions.dev scale) ----------------
   One timing vocabulary for the small transitions: menus, icon swaps,
   toasts, status lines, digits, list entries and the hero reveal. The
   big settled interactions — sheet physics, morphs, FLIP rows, the map
   and search panels — keep their own tuned curves and stay out of this. */
:root {
  --duration-stagger: 40ms;
  --duration-micro: 80ms;
  --duration-quick: 150ms;
  --duration-fast: 250ms;
  --duration-medium: 350ms;
  --duration-slow: 400ms;
  --duration-very-slow: 500ms;
  --ease-smooth-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: ease-in-out;
  --ease-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);
  --distance-micro: 4px;
  --distance-small: 6px;
  --distance-base: 8px;
  --distance-medium: 12px;
  --blur-small: 2px;
  --blur-medium: 3px;
  /* texts reveal (page heroes) */
  --stagger-dur: var(--duration-very-slow);
  --stagger-distance: var(--distance-medium);
  --stagger-stagger: var(--duration-stagger);
  --stagger-blur: var(--blur-medium);
  --stagger-ease: var(--ease-smooth-out);
  /* icon swap (map button, gear, theme, camera button) */
  --icon-swap-dur: var(--duration-fast);
  /* Two faces share one cell and cross-fade. Painted at once they were
     both partly opaque through the middle of the swap — 0.77 over 0.23
     — and one's strokes showed through the other, so the outline came
     out doubled for about a tenth of a second. Each gets half the
     clock to itself instead: the leaving face clears, then the
     arriving one comes up. */
  --icon-swap-blur: var(--blur-small);
  --icon-swap-start-scale: 0.25;
  --icon-swap-ease: var(--ease-in-out);
  /* number pop-in (live tab stats, quest tally) */
  --digit-dur: var(--duration-very-slow);
  --digit-distance: var(--distance-base);
  --digit-stagger: 70ms;
  --digit-blur: var(--blur-small);
  --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
  --digit-dir-x: 0;
  --digit-dir-y: 1;
  /* dropdown (settings menu) */
  --dropdown-open-dur: var(--duration-fast);
  --dropdown-close-dur: var(--duration-quick);
  --dropdown-pre-scale: 0.97;
  --dropdown-closing-scale: 0.99;
  /* toast: drops in from under the top bar */
  --toast-open: var(--duration-medium);
  --toast-close: var(--duration-fast);
  --toast-distance: 18px;
  --toast-blur: var(--blur-small);
  --toast-scale: 0.97;
  /* text swap (status lines) */
  --text-swap-dur: var(--duration-quick);
  --text-swap-translate-y: var(--distance-micro);
  --text-swap-blur: var(--blur-small);
  /* error shake (sign-in, forms) */
  --shake-distance: var(--distance-small);
  --shake-overshoot: 4px;
  --shake-dur-a: 80ms;
  --shake-dur-b: 60ms;
  /* skeleton reveal (live tab first load) */
  --pulse-dur: 1000ms;
  --pulse-min: 0.5;
  --reveal-dur: var(--duration-slow);
  --reveal-blur: var(--blur-small);
  --reveal-ease: var(--ease-in-out);
}

/* ---------------- Small shared transitions ----------------
   .t-rise: an element that has just appeared rises into place.
   .t-rise-list: the first eight children rise in on a stagger (the
   rest are below the fold and simply appear).
   .t-swap: a status line whose text changed; site.js re-adds
   .is-swapping so the new text enters from below with a blur.
   .t-digit-group: a number whose digits pop in when it changes.
   .is-shaking: the error shake, replayed by site.js. */
.t-rise { animation: t-rise var(--duration-fast) var(--ease-smooth-out) both; }
.t-rise-list > :nth-child(-n+8) { animation: t-rise var(--duration-fast) var(--ease-smooth-out) both; }
.t-rise-list > :nth-child(2) { animation-delay: calc(var(--duration-stagger) * 1); }
.t-rise-list > :nth-child(3) { animation-delay: calc(var(--duration-stagger) * 2); }
.t-rise-list > :nth-child(4) { animation-delay: calc(var(--duration-stagger) * 3); }
.t-rise-list > :nth-child(5) { animation-delay: calc(var(--duration-stagger) * 4); }
.t-rise-list > :nth-child(6) { animation-delay: calc(var(--duration-stagger) * 5); }
.t-rise-list > :nth-child(7) { animation-delay: calc(var(--duration-stagger) * 6); }
.t-rise-list > :nth-child(8) { animation-delay: calc(var(--duration-stagger) * 7); }
@keyframes t-rise {
  from { opacity: 0; transform: translateY(var(--distance-small)); }
  to { opacity: 1; transform: none; }
}

span.t-swap { display: inline-block; }
.t-swap.is-swapping { animation: t-swap-in var(--text-swap-dur) var(--ease-in-out) both; }
@keyframes t-swap-in {
  from { opacity: 0; transform: translateY(var(--text-swap-translate-y)); filter: blur(var(--text-swap-blur)); }
  to { opacity: 1; transform: none; filter: blur(0); }
}

@keyframes t-digit-pop-in {
  0% {
    transform: translate(
      calc(var(--digit-distance) * var(--digit-dir-x)),
      calc(var(--digit-distance) * var(--digit-dir-y))
    );
    opacity: 0;
    filter: blur(var(--digit-blur));
  }
  100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
}
.t-digit { display: inline-block; will-change: transform, opacity, filter; }
.t-digit-group.is-animating .t-digit { animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both; }
.t-digit-group.is-animating .t-digit[data-stagger="1"] { animation-delay: var(--digit-stagger); }
.t-digit-group.is-animating .t-digit[data-stagger="2"] { animation-delay: calc(var(--digit-stagger) * 2); }

.is-shaking { animation: t-input-shake calc(var(--shake-dur-a) * 2 + var(--shake-dur-b) * 2) linear; }
@keyframes t-input-shake {
  0% { transform: translateX(0); animation-timing-function: var(--ease-smooth-out); }
  28.57% { transform: translateX(var(--shake-distance)); animation-timing-function: var(--ease-smooth-out); }
  57.14% { transform: translateX(calc(var(--shake-distance) * -1)); animation-timing-function: var(--ease-smooth-out); }
  78.57% { transform: translateX(var(--shake-overshoot)); animation-timing-function: var(--ease-smooth-out); }
  100% { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .t-rise, .t-rise-list > *, .t-swap.is-swapping, .t-digit-group .t-digit, .is-shaking { animation: none !important; }
}

* { box-sizing: border-box; }

/* The document itself never scrolls: content scrolls (and rubber-bands)
   inside the body instead, so the bounce can never drag the fixed top
   bar. Same architecture the installed app uses. */
html {
  height: 100%;
  overflow: hidden;
  overscroll-behavior-y: none; /* horizontal stays native: desktop keeps its browser swipes */
  /* Phone browsers are allowed to inflate text on their own when the
     layout changes under them — opening the quest sheet over the page
     was enough to make the page behind it resize its own type, and
     closing it put the type back. The layout is already sized for
     phones, so the browser is told to leave it alone. The Text size
     setting still works: it scales the root font size instead. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
body {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior-y: contain; /* keep the bounce, stop chaining/pull-to-refresh */
}

body.themed {
  margin: 0;
  font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif; /* the signage face */
  background: var(--page-bg);
  color: var(--text);
  overflow-x: hidden; /* stray wide content must never side-scroll the page */
}

button, input, select, textarea { font: inherit; }

/* Buttons carry their own pressed styles, so the browser's tap flash
   (Safari's grey wash, Android's highlight box) only doubles up */
* { -webkit-tap-highlight-color: transparent; }
button { cursor: pointer; }

.is-hidden { display: none !important; }

/* The page grain is gone. It was a fixed noise tile over everything,
   and it read as a dirty screen rather than as paper — most of all in
   dark mode, where it sat at a higher opacity against the darker
   ground. Nothing paints over the page now.
   `html::after` is free; `body::after` is NOT — that slot is the quest
   page's sheet dim, and a full-screen overlay put there once merged
   the two into a dim on top of the open quest panel. */

/* Route bullet: the coloured disc with a letter in it. The one shape
   on the site that is allowed to be round. */
.bullet {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.7em;
  height: 1.7em;
  border-radius: 50%;
  background: var(--line-green);
  color: #ffffff;
  font-weight: 700;
  font-style: normal;
  line-height: 1;
  flex: none;
  vertical-align: middle;
}
.bullet-orange { background: var(--line-orange); }
.bullet-yellow { background: var(--line-yellow); color: #111111; }
.bullet-green { background: var(--line-green); }
.bullet-blue { background: var(--line-blue); }

/* ---------------- Top navigation bar ---------------- */

.site-nav {
  position: fixed; /* outside the body scroller, so it never bounces */
  top: 0;
  left: 0;
  right: 0;
  z-index: 1100;
  background: var(--bar-bg);
  color: var(--bar-ink);
  /* The hairline is a border rather than a layer under the bar, so the
     height site.js measures into --nav-h carries it and the content
     below starts clear of it without anything being written down. */
  border-bottom: 1px solid var(--bar-edge);
  font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif; /* the signage face */
}

/* Used on the season 1 archive page so its own sticky header keeps working */
.site-nav--static { position: static; }

.site-nav-inner {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 14px;
  padding: 12px 0;
  position: relative;
}

.site-brand {
  margin-right: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--bar-ink);
  font-weight: 850;
  letter-spacing: -0.03em;
  white-space: nowrap;
}

.site-brand-badge {
  background: var(--accent-solid);
  color: #ffffff;
  border-radius: 2px;
  padding: 5px 11px;
  font-weight: 900;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}

.site-brand-name { font-size: 0.96rem; }

/* The app bar's own title. On the web the brand is the brand and this
   is not drawn at all; in the app layout the bar is the page's title
   bar, so the page's name takes the brand's place and the page stops
   drawing it a second time (html.is-pwa .app-titled, below). The home
   page carries neither class, so it keeps the brand in the bar and its
   own board under it. The name is in the markup rather than written in
   by script, so the bar is right at first paint. */
.site-brand-page { display: none; font-size: 0.96rem; }

.site-menu {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* Each destination is a route: a coloured bullet dot in front of a
   white Helvetica label, straight off the platform signs */
.site-menu a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--bar-muted);
  font-weight: 700;
  font-size: 0.92rem;
  /* the same height as the bell and the gear beside it, so the
     inverted chip on the current page sits in a row of one height */
  min-height: 40px;
  padding: 0 13px;
  border-radius: 2px;
  transition: color var(--duration-quick) var(--ease-smooth-out), background var(--duration-quick) var(--ease-smooth-out);
  white-space: nowrap;
}

/* No disc beside every name. A route bullet identifies a service, and
   six of them in one row identified nothing — the colour was carried,
   but a reader looking at six coloured discs reads decoration, and
   then reads the discs that DO mean something, three of them on the
   whole site, as decoration too. The colour stays: it is under the
   name you are on, and under any name the pointer is over. */
.site-menu a {
  box-shadow: inset 0 -3px 0 transparent;
  transition: box-shadow var(--duration-quick) var(--ease-smooth-out);
}

.site-menu a:hover { box-shadow: inset 0 -3px 0 var(--route, var(--dot, var(--line-grey))); }

/* ---------------- One hue per destination ----------------
   A wayfinding language where a colour means two things is worse than
   no colour at all, and that is what the site had: Quest List was blue
   in the nav and green on the home page's link card, Event Info orange
   in one place and blue in the other. The names below are the single
   source of truth. Anything that colours a destination — the nav dot,
   a link card's rule, a card's icon — reads `--route` and nothing
   picks a colour by its position in a list. */
.route-home { --route: var(--line-green); }
.route-quests { --route: var(--line-blue); }
.route-info { --route: var(--line-orange); }
.route-welfare { --route: var(--line-red); }
.route-tab { --route: var(--line-yellow); }
.route-submit { --route: var(--line-purple); }
.route-messages { --route: var(--line-sky); }
/* Brown, not grey. Grey is the colour a control goes when it is turned
   off, and a 6px grey rule over the Season 1 card read as a link that
   had been disabled rather than as an older line. Brown is a trunk
   colour in the same palette and says "the one before this one". */
.route-archive { --route: var(--line-brown); }

.site-menu a:nth-child(1) { --dot: var(--line-green); }
.site-menu a:nth-child(2) { --dot: var(--line-blue); }
.site-menu a:nth-child(3) { --dot: var(--line-orange); }
.site-menu a:nth-child(4) { --dot: var(--line-red); }
.site-menu a:nth-child(5) { --dot: var(--line-yellow); }
.site-menu a:nth-child(6) { --dot: var(--line-purple); }
.site-menu a:nth-child(7) { --dot: var(--line-brown); }

.site-menu a:hover {
  color: var(--bar-ink);
  /* the page's own ground, which is a shade darker than the bar in
     both themes, so one value reads as pressed in either */
  background: var(--page-bg);
}

/* The destination you are at is inverted, which is what a sign does —
   not underlined, which is what a browser tab does. It used to be a
   dark pill on a dark bar that could barely be seen; this is the
   opposite move and reads from across the room. */
.site-menu a.is-active {
  color: var(--bar-bg);
  background: var(--bar-ink);
  box-shadow: none;
  border-radius: 2px;
}

.site-menu a.is-active:hover { color: var(--bar-bg); background: var(--bar-ink); }

/* The burger is the third icon in the row at the right end of the bar,
   sized and spaced like the bell and the gear beside it. No label and
   no outline: it used to be an outlined pill reading "Menu", which was
   the only object on the bar that was neither the brand nor an icon.
   The border stays as a transparent one so the box is the same 40px as
   the other two, and so an open state could colour it — but it does
   not need one, because the bars folding into a cross already say the
   menu is open. */
.site-nav-toggle {
  display: none;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 2px;
  background: transparent;
  color: var(--bar-ink);
}

/* Light / dark mode toggle — desktop: top right of the menu bar;
   mobile web: bottom of the burger menu; installed app: pinned top right */
.theme-toggle,
.settings-toggle {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 2px;
  background: var(--surface);
  color: var(--text);
  transition: border-color var(--duration-quick) var(--ease-smooth-out);
}

/* The border is the whole of it. A glyph in the bar is not a card and
   never leaves the page, so a shadow under it was lifting something
   that has nowhere to be lifted to. */
@media (hover: hover) and (pointer: fine) {
  .theme-toggle:hover,
  .settings-toggle:hover { border-color: var(--accent); }
}

/* both glyphs share one cell and cross-fade (icon swap): the gear
   turns as it fills, the moon and sun rotate past each other */
.theme-toggle svg,
.settings-toggle svg {
  grid-area: 1 / 1;
  width: 19px;
  height: 19px;
  /* All three properties run the same clock, so the two faces cross
     over rather than handing over. They counter-rotate through the
     same poses, so at the midpoint they are turned and scaled exactly
     alike and read as one object: what the eye follows is a shape
     filling in, not two shapes swapping. Half a clock each, with the
     arriving face waiting for the other to clear, left a frame where
     neither was drawn — measured at 0.01 combined opacity — and on the
     black bar, where the button has no border of its own, that read as
     the icon blinking out. */
  will-change: opacity, filter, transform;
}

/* controls that sit on the black bar go transparent with white ink */
.site-nav .settings-toggle {
  background: transparent;
  border-color: transparent;
  color: var(--bar-ink);
}
@media (hover: hover) and (pointer: fine) {
  .site-nav .settings-toggle:hover { border-color: var(--bar-ink); }
}
/* An open menu says so on its own, rather than leaning on the hover a
   tap happens to leave behind: on a phone the invisible haptic switch
   takes the pointer, so the gear never sees that hover at all. */

/* gear goes solid while the menu is open */
.settings-toggle { order: 10; }
/* The burger is the last of the three, at the very end of the bar: the
   bell and the gear are where they are on every page, and the control
   that opens the rest of the site sits outside them. DOM order keeps it
   before the menu it controls, so `order` is what moves it. */
.site-nav-toggle { order: 11; }
.settings-toggle .icon-gear-fill,
.settings-toggle.is-open .icon-gear-outline {
  opacity: 0;
  visibility: hidden;
  filter: blur(var(--icon-swap-blur));
  transform: rotate(45deg) scale(0.6);
}
.settings-toggle .icon-gear-outline,
.settings-toggle.is-open .icon-gear-fill {
  opacity: 1;
  visibility: visible;
  filter: blur(0);
  transform: rotate(0) scale(1);
}

/* The theme switch cross-fades instead of snapping: while site.js
   holds this class (one beat around the data-theme flip) every element
   eases its colours. Opacity, filter and transform ride along at the
   same clock so the moon/sun cross-fade still moves; the class comes
   off right after, so nothing keeps these transitions. */
html.theme-fading,
html.theme-fading *,
html.theme-fading *::before,
html.theme-fading *::after {
  transition:
    background-color var(--duration-fast) var(--ease-in-out),
    border-color var(--duration-fast) var(--ease-in-out),
    color var(--duration-fast) var(--ease-in-out),
    box-shadow var(--duration-fast) var(--ease-in-out),
    opacity var(--duration-fast) var(--ease-in-out),
    filter var(--duration-fast) var(--ease-in-out),
    fill var(--duration-fast) var(--ease-in-out),
    stroke var(--duration-fast) var(--ease-in-out),
    transform var(--duration-fast) var(--ease-in-out) !important;
}

/* Where the browser has view transitions the flip goes through one
   instead: the old rendering is snapshotted and cross-faded to the new
   one, so everything moves together rather than each element easing its
   own colours. This only sets the clock; the cross-fade is the default. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--duration-fast);
  animation-timing-function: var(--ease-in-out);
}
@media (prefers-reduced-motion: reduce) {
  html.theme-fading,
  html.theme-fading *,
  html.theme-fading *::before,
  html.theme-fading *::after { transition: none !important; }
}

/* Messages bell — sits next to the gear on the black bar and wears a
   dot while OrgComm has posted something this device has not opened.
   Round is reserved for route bullets and small status discs like this
   one; the ring is the page ground, so the dot reads clear of the bell
   in either theme. */
.bell-toggle {
  order: 9;
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 2px;
  background: transparent;
  color: var(--bar-ink);
  text-decoration: none;
  transition: border-color var(--duration-quick) var(--ease-smooth-out);
}

@media (hover: hover) and (pointer: fine) {
  .bell-toggle:hover { border-color: var(--bar-ink); }
}

/* the tap also sets the border solid, and the messages page keeps it
   solid — without this the hover highlight died with the page switch */

/* both bells share one cell and cross-fade (icon swap), the same move
   as the gear: the open bell is the resting face, the solid one fades
   up when the bell is tapped — is-filled goes on at the tap so the fill
   rings in on the way to the messages page, and that page carries the
   class in its own markup, so the bell stands solid while you are there */
.bell-toggle svg {
  grid-area: 1 / 1;
  width: 19px;
  height: 19px;
  /* All three properties run the same clock, so the two faces cross
     over rather than handing over. They counter-rotate through the
     same poses, so at the midpoint they are turned and scaled exactly
     alike and read as one object: what the eye follows is a shape
     filling in, not two shapes swapping. Half a clock each, with the
     arriving face waiting for the other to clear, left a frame where
     neither was drawn — measured at 0.01 combined opacity — and on the
     black bar, where the button has no border of its own, that read as
     the icon blinking out. */
  will-change: opacity, filter, transform;
}
.bell-toggle .icon-bell-fill,
.bell-toggle.is-filled .icon-bell-outline {
  opacity: 0;
  visibility: hidden;
  filter: blur(var(--icon-swap-blur));
  transform: scale(0.6);
}
.bell-toggle .icon-bell-outline,
.bell-toggle.is-filled .icon-bell-fill {
  opacity: 1;
  visibility: visible;
  filter: blur(0);
  transform: scale(1);
}

/* The bell's and the gear's faces cross-fade only once the page has
   loaded (html.chrome-ready, site.js). A style change made while a page
   is still arriving would otherwise play as a fade out of a face the
   page never meant to show: on an iPhone, opening the submit page drew
   the solid bell and the solid gear and faded them back to outlines.
   The face that is not showing is hidden outright as well as clear, and
   visibility rides the same clock, so during a swap both faces are
   drawn and at rest only one can be. */
html.chrome-ready .settings-toggle svg,
html.chrome-ready .bell-toggle svg {
  transition:
    opacity    var(--icon-swap-dur) var(--icon-swap-ease),
    filter     var(--icon-swap-dur) var(--icon-swap-ease),
    transform  var(--icon-swap-dur) var(--icon-swap-ease),
    visibility var(--icon-swap-dur) var(--icon-swap-ease);
}

/* the tap rings it: both faces swing from the hanger and settle, the
   swing decaying, while the solid face fades up underneath. The swing
   is the separate `rotate` property so it composes with the scale the
   cross-fade is tweening on `transform`. Only the tap (`is-ringing`,
   set in site.js) rings — the messages page's own bell is filled in
   its markup and must not swing on every load. */
.bell-toggle.is-ringing svg {
  transform-origin: 50% 2px;
  animation: bell-ring 800ms var(--ease-smooth-out);
}
@keyframes bell-ring {
  0% { rotate: 0deg; }
  15% { rotate: 16deg; }
  35% { rotate: -12deg; }
  55% { rotate: 7deg; }
  75% { rotate: -3deg; }
  100% { rotate: 0deg; }
}
@media (prefers-reduced-motion: reduce) {
  .bell-toggle.is-ringing svg { animation: none; }
}

.bell-dot {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--line-red);
  /* the bar's own colour, so the badge reads as sitting clear of the
     button rather than straddling its outline in a third colour */
  border: 2px solid var(--bar-bg);
}

.bell-dot[hidden] { display: none; }

/* ---------------- The settings menu ----------------
   Three groups of rows behind the gear: Display (appearance, text
   size, the quest animation), On the day (notifications, the offline
   map) and Phone (the app layout and haptic feedback, which only a
   handheld can offer, so the whole group goes where they go — those
   rules sit with the hour bars, near the foot of this file). A row is
   a name, one muted line saying what the setting does, and the control
   beside both; rows sit on hairlines and a group is set apart by a
   label in the site's label voice. Every control is the rocker's
   width, so the right-hand column reads as one column. The panel
   scrolls inside itself where a screen is too short for it — a phone
   on its side, an SE — instead of running off the bottom with no way
   to reach the last rows. */
.settings-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 1250;
  width: 340px;
  max-width: calc(100vw - 32px);
  /* svh where it is understood: on a phone 100vh is the tallest the
     viewport ever gets, and a panel measured against that sits under
     the browser's own bar */
  max-height: calc(100vh - var(--nav-h, 60px) - 24px);
  max-height: calc(100svh - var(--nav-h, 60px) - 24px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 2px;
  box-shadow: var(--shadow);
  padding: 12px 14px 6px;
  font-size: 0.92rem;
}
/* The app layout has a bar along the bottom as well, measured into
   --tabbar-h with its safe area, and the panel stops short of it. The
   installed app's top bar is not measured at all: it is 52px under the
   notch. The floating button is not in the sum, because it steps back
   with the veil while the menu is open (with the button's own rules). */
html.is-pwa .settings-panel {
  max-height: calc(100vh - var(--nav-h, 52px) - 24px - var(--tabbar-h, 57px));
  max-height: calc(100svh - var(--nav-h, 52px) - 24px - var(--tabbar-h, 57px));
}
html.is-installed .settings-panel {
  max-height: calc(100vh - 76px - env(safe-area-inset-top, 0px) - var(--tabbar-h, 57px));
  max-height: calc(100svh - 76px - env(safe-area-inset-top, 0px) - var(--tabbar-h, 57px));
}

/* Opens as a dropdown growing from the gear; closes quicker than it opens */
.settings-panel { transform-origin: top right; }
.settings-panel:not(.is-hidden):not(.is-closing) { animation: settings-open var(--dropdown-open-dur) var(--ease-smooth-out); }
@keyframes settings-open {
  from { opacity: 0; transform: translateY(-4px) scale(var(--dropdown-pre-scale)); }
  to { opacity: 1; transform: none; }
}

/* Closing eases out (quicker than opening, dropdown-style) instead of
   vanishing: the script holds .is-closing for the duration before
   display:none lands */
.settings-panel.is-closing {
  animation: none;
  opacity: 0;
  transform: translateY(-4px) scale(var(--dropdown-closing-scale));
  pointer-events: none;
  transition:
    opacity   var(--dropdown-close-dur) var(--ease-smooth-out),
    transform var(--dropdown-close-dur) var(--ease-smooth-out);
}

/* While the menu is open the page content dims behind a veil. The bars
   and floating controls all sit above z-index 1000, so only the content
   fades; clicks pass through it and close the menu as usual. */
html.settings-open body::before,
html.settings-closing body::before {
  content: '';
  position: fixed;
  inset: 0;
  background: rgba(12, 15, 20, 0.22);
  z-index: 1000;
  pointer-events: none;
  animation: settings-veil var(--dropdown-open-dur) var(--ease-smooth-out);
  transition: opacity var(--dropdown-close-dur) var(--ease-smooth-out);
}
/* while the menu eases out, the veil fades with it */
html.settings-closing body::before {
  opacity: 0;
  animation: none;
}
@keyframes settings-veil {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .settings-panel:not(.is-hidden),
  html.settings-open body::before { animation: none; }
  .settings-panel.is-closing,
  html.settings-closing body::before { transition: none; }
}

.settings-group + .settings-group { margin-top: 14px; }

/* a group's label, in the voice of DATE and HOURS on the home board */
.settings-group-title {
  margin: 0 0 2px;
  color: var(--muted);
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: 0.09em;
  line-height: 1.3;
  text-transform: uppercase;
}

/* The name over the line that explains it, the control beside both.
   The hairline is under a row, not over it, and the last row in a
   group carries none: a row that hides itself (App layout in the
   installed app) would otherwise leave its line between the group's
   label and the row that is still there. */
.settings-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 14px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.settings-row:last-child { border-bottom: 0; }

.settings-name {
  grid-column: 1;
  grid-row: 1;
  font-weight: 650;
  line-height: 1.35;
}

.settings-desc {
  grid-column: 1;
  grid-row: 2;
  margin: 2px 0 0;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.45;
}

.settings-row > .theme-toggle,
.settings-row > .settings-size,
.settings-row > .settings-knob {
  grid-column: 2;
  grid-row: 1 / span 2;
}

/* The rocker's tap target is the switch, not the row: the quest
   list's rule, for the quest list's reason. A label across the whole
   row flips the switch when a finger is put down on the words to
   scroll, and this panel scrolls on a short screen. The label round
   the switch is 44px tall so a thumb still has something to land on. */
.settings-knob {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 44px;
  cursor: pointer;
}
.settings-knob input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
/* a switch that cannot be used — notifications in a browser without
   them — looks it, rather than sitting there like the others */
.settings-knob input:disabled + .settings-switch { opacity: 0.4; }

/* The O/I rocker in place of a browser tick: a paper box with a solid
   block over one half, grey over the O when off and accent green over
   the I when on. The letters are set in the site's label voice
   (--t-micro, 800) and the box is measured in em, so the text-size
   ladder scales the whole switch rather than growing the letters inside
   fixed cells. No shadow: a control that is set is not lifted off the
   paper, and the size buttons beside it in the menu carry none. */
/* The light/dark button sits in the same column as the rockers below
   it, so it is one of their width rather than a 40px square pushed to
   the right edge. Both were right-aligned, which put the glyph 11px
   off the column's centre line — near enough to read as a mistake.
   The width is the rocker's own arithmetic (two 2.6em tracks at
   --t-micro plus its two hairlines), so the two stay in step all the
   way along the text-size ladder instead of only at 100%. */
.settings-panel .theme-toggle {
  font-size: var(--t-micro);
  width: calc(5.2em + 2px);
  /* boxed like the rocker and the stepper under it, so the column is
     three controls of one family rather than two boxes and a glyph */
  border-color: var(--border-strong);
}

.settings-switch {
  flex: none;
  position: relative;
  display: inline-grid;
  grid-auto-flow: column;
  /* both halves the same width, so the filled block can travel between
     them: O is a much wider glyph than I, and equal tracks are what
     let one block land squarely on either side */
  grid-auto-columns: 1fr;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  overflow: hidden;
  background: var(--surface);
  font-size: var(--t-micro);
  font-weight: 800;
  line-height: 1;
  user-select: none;
}

/* The filled half is one block that slides, instead of two cells
   cross-fading their backgrounds. It overshoots a little as it lands
   and the switch's own edge clips the overshoot, so the block reads as
   settling into its side. */
.settings-switch::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 50%;
  background: var(--border-strong);
  transition:
    transform var(--duration-fast) var(--ease-bounce),
    background-color var(--duration-fast) var(--ease-smooth-out);
}

input:checked + .settings-switch::before {
  transform: translateX(100%);
  background: var(--accent-solid);
}

/* The letters stay where they are — they are position labels, and a
   switch whose O and I moved would stop reading as one. They sit over
   the block and flip colour as it arrives: the uncovered letter is
   muted on the paper, and the covered one takes the ink that reads on
   its block — the text ink on the grey, the accent's own ink on the
   green. A paper-coloured O on the grey block measured 3.3:1 on paper
   and 1.9:1 in the dark theme, where it all but vanished. */
.settings-switch span {
  position: relative;
  min-width: 2.6em;
  padding: 0.6em 0;
  text-align: center;
  color: var(--muted);
  /* uncovered: back to muted at once, as the block leaves */
  transition: color var(--duration-micro) var(--ease-in-out);
}

/* the switch squeezes under the finger while a label is pressed */
.settings-switch { transition: transform var(--duration-quick) var(--ease-smooth-out); }
.settings-knob:active .settings-switch,
.sub-check:active .settings-switch,
.ql-checkbox:active .settings-switch { transform: scale(0.94); }

/* State selectors stay unscoped so any hidden checkbox followed by a
   switch gets the treatment (settings menu and quest panel both use it) */
/* covered: the flip waits for the block to get there */
input:not(:checked) + .settings-switch .switch-o,
input:checked + .settings-switch .switch-i {
  transition: color var(--duration-micro) var(--ease-in-out) 140ms;
}

input:not(:checked) + .settings-switch .switch-o { color: var(--text); }
input:checked + .settings-switch .switch-i { color: var(--on-accent); }

@media (prefers-reduced-motion: reduce) {
  .settings-switch::before,
  .settings-switch span { transition: none !important; }
}

/* ---- Direct-tap haptics (iOS 26.5 and later) ----
   iOS only buzzes for a finger landing on a real native switch, so
   site.js lays invisible ones over the controls worth a tick. They have
   to stay rendered with their native appearance — opacity is the only
   thing hiding them — or the tick goes with them. */
html.haptic-taps .haptic-tap {
  opacity: 0;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
}

/* laid over a button or a link, which cannot hold a switch inside it:
   site.js writes the box from the control's own layout position */
html.haptic-taps .haptic-tap-over {
  position: absolute;
  z-index: 2;
}

/* the row the light/dark button sits in is the box its switch is
   measured against */
html.haptic-taps .settings-row { position: relative; }

/* the app's bottom bar: one switch per link, in the link's own cell */
html.haptic-taps .tab-bar .haptic-tap {
  position: relative;
  z-index: 2;
  width: 100%;
  height: 100%;
}

/* the floating button: the wrap's box is exactly the button's box */
html.haptic-taps .fab-wrap .haptic-tap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
}

/* the press squeeze moves up to the wrap, since the tap now lands on
   the switch rather than on the button itself */
html.haptic-taps .fab-wrap:active .fab-btn { transform: scale(0.93); }

/* A rocker needs no overlay: its own checkbox is the switch, grown
   from its corner pixel to cover the control, so the finger lands on
   the native switch rather than on the label that forwards to it. In
   the settings menu the control is the knob, the label round the drawn
   switch. The checkbox takes z-index 2: the drawn switch is positioned
   and comes later in the document, so without it the finger lands on
   the drawing and the tick never comes. */
html.haptic-taps .settings-knob,
html.haptic-taps .sub-check,
html.haptic-taps .ql-checkbox { position: relative; }

html.haptic-taps .settings-knob > input[type="checkbox"],
html.haptic-taps .sub-check > input[type="checkbox"],
html.haptic-taps .ql-checkbox > input[type="checkbox"] {
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
}
html.haptic-taps .settings-knob > input[type="checkbox"],
html.haptic-taps .sub-check > input[type="checkbox"] { z-index: 2; }

input:focus-visible + .settings-switch {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The text-size stepper is a rocker with two directions rather than
   two loose buttons with a number between them: A− and A+ in one box
   the rocker's width, so the right-hand column stays one column, and
   the size itself reads on the row's second line, where the other
   rows keep their explanation. A press fills the half the finger is
   on, the way the rocker's block fills a side; the end of the ladder
   is greyed, since a press past it did nothing before and now looks
   like it. */
.settings-size {
  position: relative;
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  width: calc(5.2em + 2px);
  height: 40px;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  background: var(--surface);
  font-size: var(--t-micro);
}

.settings-size-btn {
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--duration-quick) var(--ease-smooth-out), color var(--duration-quick) var(--ease-smooth-out);
}
.settings-size-btn + .settings-size-btn { border-left: 1px solid var(--border-strong); }
@media (hover: hover) and (pointer: fine) {
  .settings-size-btn:hover:not(:disabled) { background: var(--page-bg); }
}
.settings-size-btn:active:not(:disabled) { background: var(--border-strong); }
.settings-size-btn:disabled { color: var(--border-strong); cursor: default; }
.settings-size-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* the size itself, on the row's line */
#set-font-val { font-variant-numeric: tabular-nums; }

/* The fullscreen quest map owns the whole screen: the site chrome
   steps aside so the map's own buttons are reachable everywhere */
html.ql-map-full .site-nav,
html.ql-map-full .site-footer,
html.ql-map-full .fab-wrap,
html.ql-map-full .tab-bar { display: none; }

.theme-toggle .icon-sun,
html[data-theme="dark"] .theme-toggle .icon-moon {
  opacity: 0;
  filter: blur(var(--icon-swap-blur));
  transform: rotate(-90deg) scale(0.5);
}
.theme-toggle .icon-moon,
html[data-theme="dark"] .theme-toggle .icon-sun {
  opacity: 1;
  filter: blur(0);
  transform: rotate(0) scale(1);
}

/* Drawn to the same optical size as the 19px bell and gear it now sits
   beside, rather than to the old pill's label height. */
.nav-burger {
  position: relative;
  display: inline-block;
  width: 18px;
  height: 14px;
}

.nav-burger span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 1px;
  background: currentColor;
  transition: transform var(--duration-fast) var(--ease-smooth-out), opacity var(--duration-fast) var(--ease-smooth-out);
}

.nav-burger span:nth-child(1) { top: 0; }
.nav-burger span:nth-child(2) { top: 6px; }
.nav-burger span:nth-child(3) { top: 12px; }
.site-nav-toggle[aria-expanded="true"] .nav-burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.site-nav-toggle[aria-expanded="true"] .nav-burger span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.site-nav-toggle[aria-expanded="true"] .nav-burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 840px) {
  .site-nav-inner { flex-wrap: wrap; }
  .site-nav-toggle { display: inline-grid; }
  .site-menu { order: 20; }
  .site-menu {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 8px 0 10px;
    border-top: 1px solid var(--border);
    margin-top: 10px;
  }
  .site-menu.is-open { display: flex; }
  .site-menu a { padding: 12px 14px; border-radius: 2px; }
  /* the dropped-open burger menu is part of the black sign */
  .site-menu { border-top-color: rgba(255, 255, 255, 0.22); }
  /* site.js grows the bar to fit; the rows rise in behind it */
  .site-menu.is-open a { animation: t-rise var(--duration-fast) var(--ease-smooth-out) both; }
  .site-menu.is-open a:nth-child(2) { animation-delay: calc(var(--duration-stagger) * 1); }
  .site-menu.is-open a:nth-child(3) { animation-delay: calc(var(--duration-stagger) * 2); }
  .site-menu.is-open a:nth-child(4) { animation-delay: calc(var(--duration-stagger) * 3); }
  .site-menu.is-open a:nth-child(5) { animation-delay: calc(var(--duration-stagger) * 4); }
  .site-menu.is-open a:nth-child(6) { animation-delay: calc(var(--duration-stagger) * 5); }
  .site-menu.is-open a:nth-child(7) { animation-delay: calc(var(--duration-stagger) * 6); }
  .site-menu.is-closing a { animation: none; }
  @media (prefers-reduced-motion: reduce) {
    .site-menu.is-open a { animation: none !important; }
  }
}

@media (max-width: 430px) {
  .site-brand-name { display: none; }
  /* Installed app: the centred brand is the app title, so keep it visible */
  html.is-pwa .site-brand-name { display: inline; }
}

/* ---------------- Page scaffolding ---------------- */

.page-shell {
  --shell-top: 32px;
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  padding: var(--shell-top) 0 56px;
}

/* On the web the top bar and footer sit outside the body scroller (fixed),
   so the content is spaced by their measured heights (--nav-h/--footer-h,
   kept exact by site.js; the body itself stays unpadded because scroller
   padding shifts position: sticky offsets). The content column paints on
   an opaque full-bleed background and slides over the footer, which is
   revealed only at the end of the page: the bounce can move the content
   but never the bars. The installed app sizes its chrome separately. */
html:not(.is-pwa) body.themed > main.page-shell {
  width: auto;
  margin: var(--nav-h, 65px) 0 0;
  padding-left: max(16px, env(safe-area-inset-left, 0px) + 16px, calc((100% - 1180px) / 2));
  padding-right: max(16px, env(safe-area-inset-right, 0px) + 16px, calc((100% - 1180px) / 2));
  background: var(--page-bg);
  position: relative;
  z-index: 1;
}
html:not(.is-pwa) body.themed:has(> .site-footer) > main.page-shell {
  margin-bottom: var(--footer-h, 160px);
}
html:not(.is-pwa) body.themed > .site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  margin-top: 0;
  z-index: 0;
}

/* The hero is a stack of station signs: black bars, white Helvetica,
   route bullets, a wayfinding arrow — the poster wall look. */
.hero {
  display: block;
  border: none;
  background: transparent;
  padding: 22px 0 6px;
  margin-bottom: 12px;
  text-align: left;
}

/* ---------------- The station headwall ----------------
   One plate. The diagram is the whole field, drawn so the lower left of
   it is empty, and the name and the four facts that decide whether
   somebody turns up — date, time, place, price — are set into that
   space rather than parked in a clear band below the picture. The
   facts sit on columns 1 to 8 of the page's own twelve, two columns
   each, which leaves the right third of the plate to the fan. */
.board {
  --bleed-m: calc(50% - 50vw);
  --bleed-p: calc(50vw - 50%);
  display: block;
  margin-inline: var(--bleed-m);
  padding: 34px var(--bleed-p) 0;
  /* The plate starts where the nav's route stripe ends. A band of paper
     between two blacks at the very top of the page reads as a fault
     rather than as a decision, so the shell's own top padding is given
     back here. */
  margin-top: calc(var(--shell-top, 32px) * -1);
  background: var(--sign-bg);
  color: var(--sign-ink);
  overflow: hidden;
}

/* The drawing is the top band of the plate and the type sits under it,
   rather than the type floating in the drawing's field competing with
   the routes. meet, not slice: every route on it runs to a terminal
   and slice threw the ends away as soon as the plate got wide. */
.board-map {
  display: block;
  width: 100%;
  height: auto;
}

/* The name sits under the lines that serve it and the strip sits at the
   foot, which is the grammar of the sign this whole page is built on.
   The top padding is the height of the trunk: the drawing is scaled to
   the plate's width, so 107.5 of the viewBox's 1200 units is 8.96% of
   that width, and a percentage padding resolves against exactly that.
   Everything between the two is the drawing's, and the fan runs down
   through it. */
.board-type {
  width: 100%;
  padding: 30px 0 30px;
  row-gap: 16px;
  align-content: space-between;
  color: var(--sign-ink);
}

/* `.board h1`, not `.board-name`: `.themed h1` sets the page's ink,
   and it both outranks a bare class and is written further down the
   file than an equal one — either way the name came out near-black on
   the black plate, which is invisible rather than merely wrong. */
.board h1.board-name {
  grid-column: 1 / 9;
  /* the L's side bearing, so the stem lands on the same edge as the D
     of DATE below it rather than the box landing there */
  margin: 0 0 0 -0.055em;
  color: var(--sign-ink);
  font-size: var(--t-display);
  font-weight: 850;
  line-height: 0.93;
  letter-spacing: -0.038em;
  text-wrap: balance;
}

/* No dividers. On a signage plate a rule is full-weight white or it is
   not there; hairline grey cells made the four facts read as an HTML
   table sitting on the diagram. Space does the work. */
/* One departure strip across the whole plate, not a two-by-two huddled
   in the left quarter with the countdown as a footnote beneath it. The
   cells are items of the plate's own twelve columns rather than a grid
   nested inside one, so they land on the page's column lines: two each
   for the four fixed facts, four for the one that is moving. */
.board-facts {
  display: contents;
  margin: 0;
}

/* Placed by hand, every one of them. Auto-placement fills the space
   left beside the name first, so the date and the time went to the
   right of the title and the strip broke across two rows. */
.board-facts > div:nth-child(1) { grid-column: 1 / span 2; }
.board-facts > div:nth-child(2) { grid-column: 3 / span 2; }
.board-facts > div:nth-child(3) { grid-column: 5 / span 2; }
.board-facts > div:nth-child(4) { grid-column: 7 / span 2; }
.board-facts > div:nth-child(5) { grid-column: 9 / span 4; }

/* the destinations, named at their terminals — the drawing carries the
   site rather than describing it */
.map-label {
  fill: var(--sign-ink);
  font-family: inherit;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.01em;
}

/* the time is what a departure board is for, so it is set in ink at
   the label's own size with tabular figures */
.map-label text { font-variant-numeric: tabular-nums lining-nums; }

.map-sub {
  fill: var(--sign-muted);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.03em;
}

/* a service's name, set on the run it names */
.map-route { font-size: 14px; }

/* On a phone the drawing keeps its shape but not its labels: at a
   third of the width the station names would be four pixels tall. The
   times stay, because they are what the drawing is about; the places
   are in the timetable further down the page. */
@media (max-width: 900px) {
  .map-sub, .map-route { display: none; }
  .map-label text { font-size: 30px; }
  .board-facts > div:nth-child(1) { grid-column: 1 / span 6; }
  .board-facts > div:nth-child(2) { grid-column: 7 / span 6; }
  .board-facts > div:nth-child(3) { grid-column: 1 / span 6; }
  .board-facts > div:nth-child(4) { grid-column: 7 / span 6; }
  .board-facts > div:nth-child(5) { grid-column: 1 / -1; }
  .board h1.board-name { grid-column: 1 / -1; }
}

.board-facts dt {
  color: var(--sign-muted);
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.board-facts dd {
  margin: 5px 0 0;
  font-size: var(--t-lede);
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* ---------------- The opening screen ----------------
   What it is on the left, how the day runs on the right, both on the
   page's own columns: the left block ends on column 7 and the right one
   starts on column 9, the line the third quest column starts on. */
.opening {
  align-items: start;
  margin-top: 44px;
}

.opening-main { grid-column: 1 / 8; }
.dayline { grid-column: 9 / -1; }

@media (max-width: 900px) {
  .opening-main, .dayline { grid-column: 1 / -1; }
  .dayline { margin-top: 34px; }
}

/* one primary action; the other two are where they belong, on a line
   of links under it, rather than two ghost buttons beside it */
.hero-also {
  display: inline-flex;
  gap: 20px;
  align-items: center;
  flex-wrap: wrap;
}

.hero-also a {
  color: var(--text);
  font-weight: 700;
  text-underline-offset: 3px;
}

/* No trailing arrow. Helvetica's arrow is a hairline and sat visibly
   lighter than the bold underlined text it followed; underlined text
   in a row of links already reads as a link. */

/* the lead paragraph: one step above body, the only thing on the page
   set at that size, so the opening has a voice without a subtitle */
.lead {
  margin: 0 0 18px;
  font-size: var(--t-lede);
  line-height: 1.55;
}

/* One treatment for a heading inside a column, and one for a heading
   that opens a section. The page had three at the same level — a heavy
   rule above, a thin rule below, and no rule at all — so a reader
   could not tell which headings were peers. */
.prose .opening-head,
.opening-head,
.dayline-head {
  margin: 44px 0 12px;
  padding-top: 0;
  border-top: 0;
  font-size: var(--t-lede);
  letter-spacing: -0.025em;
}

.dayline > .dayline-head:first-of-type { margin-top: 0; }

/* ---------------- The day, as a timetable ----------------
   The diagram at the top of the page already draws the day as a route,
   with the same calls on it. Drawing it a second time thirty pixels to
   the right — a vertical stroke carrying its own, different station
   dots — put two contradicting marker languages in one screen and said
   the same thing twice. A transit system has a diagram and it has a
   timetable; this is the timetable. Times in tabular figures in their
   own column, what happens beside them, one hairline per row. */
.dayline { position: relative; }

.dayline-head--second { margin-top: 44px; }

.dayline-note {
  margin: 20px 0 0;
  color: var(--muted);
  font-size: var(--t-micro);
  letter-spacing: 0.02em;
  line-height: 1.5;
}

.dayline-stops {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  border-top: 1px solid var(--border);
}

.daystop {
  display: grid;
  grid-template-columns: 4.4em minmax(0, 1fr);
  gap: 0 14px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
}

.daystop-at {
  grid-row: span 2;
  padding-top: 3px;
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--muted);
}

.daystop-name {
  font-size: var(--t-body);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.daystop-note {
  color: var(--muted);
  font-size: var(--t-small);
  line-height: 1.45;
}

/* The bars share one measure. They used to shrink-wrap their text, so
   the title bar ended at 676px and the paragraph bar under it at
   546px, and the page opened on a ragged right edge before it had said
   anything. */
.hero-board {
  display: grid;
  gap: 10px;
  justify-items: stretch;
  max-width: 860px;
}

.sign-bar {
  background: var(--sign-bg);
  color: var(--sign-ink);
  border-radius: 2px;
  box-shadow: var(--shadow);
  padding: 12px 18px;
}

.hero h1.sign-bar {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  color: var(--sign-ink);
  padding: 16px 22px;
}

/* No measure of its own: it shares the board's, so the two bars have
   one right edge. */
.hero-copy.sign-bar {
  margin: 0;
  color: var(--sign-muted);
  font-size: 0.98rem;
  line-height: 1.6;
  padding: 14px 22px;
}

/* On the paper, not reversed out of a second black bar. A signage bar
   carries a name or a direction; four lines of description is not what
   the device is for, and stacking two black blocks put the page's
   longest read at its lowest contrast. */
.hero-copy:not(.sign-bar) {
  margin: 16px 0 0;
  /* ems, not ch: ch is the width of a zero, which is far wider than
     Helvetica's average lowercase letter, so a measure meant to be
     sixty-two characters ran to seventy-three on every page */
  max-width: 31em;
  font-size: 1.06rem;
  line-height: 1.6;
}

/* The name of the thing is the biggest type on the page. It was not:
   the countdown's digits were larger than the h1, so the first thing
   the eye landed on was a clock. */
/* The name of the thing is the biggest type on the page. It was not:
   the countdown's digits were larger than the h1, so the first thing
   the eye landed on was a clock. Capped where the title and its four
   discs still make one line inside the board. */
.themed h1 {
  margin: 0;
  color: var(--text);
  font-size: clamp(2.1rem, 4.6vw, 3.6rem);
  letter-spacing: -0.045em;
  line-height: 1;
}

.themed h2 { letter-spacing: -0.03em; }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 12px 26px;
  margin-top: 10px;
}

/* ---------------- One grid ----------------
   Twelve columns and a single gutter, shared by every band on the page.
   Three-up spans four, four-up spans three, two-up spans six, and a
   sidebar starts on column nine — so the quest columns, the figures,
   the two lists and the day's running order all land on the same
   verticals instead of each carrying a column system of its own. */
.band {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
}

/* the two shapes that repeat: a main column and the narrower one
   beside it, ending and starting on the same lines as the opening */
.col-lead { grid-column: 1 / 8; }
.col-side { grid-column: 9 / -1; }

@media (max-width: 900px) {
  .col-lead, .col-side { grid-column: 1 / -1; }
  .col-side { margin-top: 34px; }
}

/* Every section used to get the same space above it, so a page read as
   a stack of equal bands with nothing given room and nothing
   compressed. A section that opens a new subject gets more; one that
   carries on from the section above it gets less. */
/* Pauses of different length, so the page has a rhythm rather than a
   stack of equal bands: a section that carries on from the one above
   it, a section that opens a new subject, and the long pause on
   either side of the black plate. Nearly a factor of two between the
   shortest and the longest. */
.section { margin-top: 48px; }
.section + .section { margin-top: 56px; }
.section-tight { margin-top: 26px; }
.plate,
.plate + .section { margin-top: 92px; }

/* The heavy rule is the top edge of a signage plate, not a paragraph
   separator. It opens a movement and there are two of them on a page;
   everywhere else the heading does the work on its own, with scale and
   the space above it saying where it sits. Two rule weights on the
   page, this one and the hairline. */
.section-heading {
  margin: 0 0 4px;
  padding-top: 12px;
  border-top: 6px solid var(--text);
  font-size: var(--t-head);
}

/* ---------------- A plate on the page ----------------
   White on black, reaching to the page's gutter. The band is what
   gives the page its rhythm: paper, signage, paper. It bleeds by a
   fixed amount that is never more than the shell's own padding, so it
   needs no viewport arithmetic and never brings a sideways scrollbar
   with it. */
.plate {
  --bleed-m: calc(50% - 50vw);
  --bleed-p: calc(50vw - 50%);
  background: var(--sign-bg);
  color: var(--sign-ink);
  border-radius: 0;
  margin-inline: var(--bleed-m);
  padding: 38px var(--bleed-p) 40px;
}

.plate-head {
  margin: 0 0 22px;
  padding-top: 12px;
  border-top: 6px solid var(--sign-ink);
  color: var(--sign-ink);
  font-size: var(--t-head);
  letter-spacing: -0.025em;
}

/* `.grid-figures .stat-note` is written further down the file at the
   same weight, so a plain `.plate .stat-note` lost to it and the
   captions came out page-ink on the black plate: invisible. */
.plate .stat-label { color: var(--sign-muted); }
.plate .stat-value,
.plate .grid-figures .stat-note { color: var(--sign-ink); }
.plate .grid-figures { border-top-color: rgba(255, 255, 255, 0.24); }

.plate-note {
  margin: 26px 0 0;
  color: var(--sign-muted);
  font-size: var(--t-small);
  line-height: 1.6;
}

.plate-note a {
  color: var(--sign-ink);
  font-weight: 800;
  text-underline-offset: 3px;
}

/* The space above a heading belongs to the heading. It used to be
   near-identical to the space below it, so every title floated between
   two sections instead of opening one. */
.section > .section-heading:first-child { margin-top: -6px; }

.section-sub {
  margin: 0 0 16px;
  color: var(--muted);
  line-height: 1.6;
  max-width: 720px;
}

/* A section's deck often carries the link the section is actually for,
   and it was the palest, smallest type in the block. It reads at body
   size now — in ink, not in the accent. Green had come to mean two
   things at once on this page: it was the Home route, the five-line
   stripe's bottom line and the SCS.2 badge, and it was also every
   link, the primary button and the floating button. A reader cannot
   tell a route from a control when both are #00933c, so green is now
   the colour of an action and nothing else. */
.section-sub a {
  color: var(--text);
  font-weight: 800;
}

.section-heading + .section-sub { margin-top: 8px; }
.section-heading + .grid,
.section-heading + .chip-row,
.section-heading + .table-scroll,
.section-heading + .card { margin-top: 14px; }
.section-sub-after { margin: 12px 0 0; }
.hero + .notice { margin-top: 0; }

/* Inline links in running text pick up the accent; buttons, cards and
   chips carry their own colours. `.prose a` is not in this list: it has
   its own rule further down and is black with an underline.
   The declaration on the last line went missing once — `.prose a` used
   to end the list and the whole line was deleted to stop it outranking
   `.btn-accent`, which left eight selectors running into `.card-copy`'s
   rule and no colour rule at all, so every one of these links fell
   through to the browser's blue. Do not delete the last selector; take
   the selector off the list and leave the declaration where it is. */
.section-sub a,
.notice a,
.card-copy a,
.card-note a,
.countdown-note a,
.credit a,
.sub-hint a,
.rg-muted a { color: var(--accent-dark); font-weight: 700; }

/* card body text and the muted example line beneath it */
.card-copy { margin: 0 0 10px; line-height: 1.6; }
.card-note { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }
.card h2 { margin: 0 0 14px; }

/* the three quest kinds on the home page: a bullet and a title */
.quest-kind {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 10px;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
}

/* small credits and sign-offs */
.credit { text-align: center; color: var(--muted); font-size: 0.88rem; }
.sign-off { text-align: center; font-weight: 800; font-size: 1.1rem; }
.sign-off span { display: block; color: var(--muted); font-weight: 600; font-size: 0.95rem; }
.quote { border-left: 4px solid var(--border); padding-left: 14px; color: var(--muted); }

/* ---------------- Buttons ---------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 2px solid var(--text);
  border-radius: 2px;
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  padding: 11px 16px;
  font-weight: 700;
  box-shadow: none;
  transition: transform var(--duration-quick) var(--ease-smooth-out), box-shadow var(--duration-quick) var(--ease-smooth-out),
    border-color var(--duration-quick) var(--ease-smooth-out), opacity var(--duration-quick) var(--ease-smooth-out);
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-hover);
}

.btn:active { transform: translate(1px, 1px); box-shadow: none; }
.btn:disabled { opacity: 0.5; cursor: default; transform: none; box-shadow: none; }

.btn-dark {
  background: var(--sign-bg);
  border-color: var(--sign-bg);
  color: var(--sign-ink);
}

.btn-accent {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent);
}

/* A button inside prose is a button. `.prose a` outranks `.btn-accent`
   on its own, so the label was repainted the link green and the
   primary action came out as a green block with nothing written on it. */
.prose a.btn { color: var(--text); }
.prose a.btn-accent { color: var(--on-accent); }

/* ---------------- Cards & grids ---------------- */

/* No shadow. The hard offset shadow is the system's way of lifting one
   thing off the paper, and it was on every box on every page — fact
   cards, quest cards, stat cards, chips, buttons and link cards all at
   once — so nothing was lifted. It is kept for the sign bars and the
   primary action and taken off everything else. */
.card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 20px;
}

/* The gutter is never smaller than the padding inside the boxes it
   separates, or a row of four reads as one box that has cracked. */
.grid { display: grid; gap: 22px; }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 900px) {
  .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .card { padding: 16px; }
}

.stat {
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 16px 18px;
  background: var(--surface);
}

.stat-label {
  color: var(--muted);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 800;
}

.stat-value {
  margin-top: 6px;
  font-weight: 850;
  font-size: clamp(1.14rem, 2.2vw, 1.5rem);
  letter-spacing: -0.035em;
}

.stat-note {
  margin-top: 4px;
  color: var(--muted);
  font-size: 0.82rem;
}

/* ---------------- Facts, as a table rather than as cards ----------
   A row of four labelled facts is a table. Boxing each one was the
   same component the figures row below it used, so the page said the
   two were the same kind of thing when one is the day's details and
   the other is last year's results. Hairline dividers, no boxes. */
.grid-facts {
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  gap: 0;
}

.grid-facts .stat {
  border: 0;
  border-left: 1px solid var(--border);
  background: transparent;
  padding: 16px 22px;
}

.grid-facts .stat:first-child { border-left: 0; padding-left: 0; }

/* ---------------- Figures ----------------
   A numbers section whose numbers are one step above body copy has
   failed at its one job. These sit on the paper at display size with
   tabular figures, so the column of them lines up. */
.grid-figures {
  row-gap: 26px;
  border-top: 1px solid var(--border);
  padding-top: 18px;
}

.grid-figures .stat {
  grid-column: span 3;
  border: 0;
  background: transparent;
  padding: 0;
}

@media (max-width: 860px) { .grid-figures .stat { grid-column: span 6; } }

.grid-figures .stat-value {
  margin-top: 2px;
  font-size: var(--t-display);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -0.055em;
  line-height: 1;
}

/* The caption under a figure is the only thing telling you what the
   figure counted, and it was the palest type in the section. */
.grid-figures .stat-note { margin-top: 8px; font-size: var(--t-small); color: var(--text); }

/* The three kinds of quest take the three colours the plate at the top
   of the page gives them, so the plate is the key to this section and
   a reader who looked at it is paid back for looking. The colour is a
   rule over the column, not a disc: a disc identifies a route, and
   these are what the routes carry. */
.route-main { --route: var(--line-blue); }
.route-side { --route: var(--line-orange); }
.route-bonus { --route: var(--line-red); }

/* ---------------- A map key ----------------
   Three kinds of quest, each under a rule. The three used to carry
   black discs lettered M, S and B — a third bullet vocabulary on a
   page that already had station circles and timeline dots, and in this
   language the letter on a disc IS the line's name, so a disc reading
   M sitting beside the words "Main quests" spends the system's
   strongest mark saying nothing. The heading carries it. */
/* A three-up comparison only compares if its parts line up: the names
   on one baseline, the copy starting on one, the examples on one.
   Left to themselves the three columns each started their second
   paragraph wherever the first one happened to end. */
.legend {
  margin-top: 18px;
  grid-template-rows: auto auto auto;
  row-gap: 0;
}

.legend-item {
  grid-column: span 4;
  grid-row: 1 / span 3;
  display: grid;
  grid-template-rows: subgrid;
  gap: 2px;
  align-content: start;
  padding: 14px 0 20px;
  border-top: 5px solid var(--route, var(--border));
}

.legend-item:not([class*="route-"]) { border-top-width: 1px; }

@media (max-width: 860px) {
  .legend { grid-template-rows: none; row-gap: 0; }
  .legend-item { grid-column: 1 / -1; grid-row: auto; grid-template-rows: none; }
}

.legend-name {
  margin: 0;
  align-self: center;
  font-size: var(--t-lede);
  letter-spacing: -0.02em;
}

.legend-item .card-copy { margin: 6px 0 10px; font-size: var(--t-body); }

/* The example is marked by its own indent and a hairline, not by a
   fifteenth uppercase micro-label. That label was how the page said
   "small thing here" regardless of what the thing was, and by the
   fourth section it had stopped being a signal. */
.legend-item .card-note {
  margin: 14px 0 0;
  padding-left: 0;
  border-left: 0;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--text);
}

.legend-item .card-note b { font-weight: 800; }

/* ---------------- A ruled list ----------------
   Names read as a list. As chips they were the same object as the
   outline buttons and promised a tap they do not have, and ten of them
   wrapped to leave one orphan on a line of its own. */
.ruled-list {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
}

.ruled-list li {
  border-top: 1px solid var(--border);
  padding: 10px 0;
  font-size: 0.96rem;
  line-height: 1.5;
  break-inside: avoid;
}

/* ---------------- A term and what it means ----------------
   The heavy rule on top and nothing between the rows: space separates
   them, which is what space is for. */
.deflist {
  margin: 18px 0 0;
  row-gap: 22px;
}

.deflist dt {
  grid-column: 1 / 5;
  font-size: var(--t-body);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.deflist dd {
  grid-column: 5 / 12;
  margin: 0;
  font-size: var(--t-body);
  line-height: 1.6;
}

@media (max-width: 760px) {
  .deflist { row-gap: 18px; }
  .deflist dt { grid-column: 1 / -1; }
  .deflist dd { grid-column: 1 / -1; margin-top: 2px; }
}

/* A list, with nothing between the rows. Ten names each under their
   own hairline put the rule to a seventh job on one page; ten names
   run together with middots made a wall that broke mid-name. Space
   separates them and each one stays whole. */
.soc-names {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  font-size: var(--t-small);
  font-weight: 700;
  line-height: 1.5;
}

.soc-names {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--gutter);
}

.soc-names li { padding: 5px 0; }

@media (max-width: 420px) { .soc-names { grid-template-columns: 1fr; } }

/* Chips (societies etc.) */
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }

/* A chip is a name, not a control. With a border, a fill and the
   system's shadow it was the same object as the outline button beside
   it and promised a tap it does not have. */
.chip {
  display: inline-block;
  border: 1px solid var(--border);
  border-radius: 2px;
  background: transparent;
  padding: 7px 12px;
  font-weight: 700;
  font-size: 0.88rem;
  color: var(--muted);
}

/* ---------------- Countdown ---------------- */

/* Left, like everything else, and smaller than the title. The board
   used to be centred on a page that is otherwise all left-aligned, and
   its digits were larger than the h1 — so the loudest thing on the
   page, and the only centred thing on it, was a clock. It is a fact
   about the day now, set beside the others. */
.countdown { text-align: left; }

.countdown h2 {
  margin: 0 0 4px;
  padding-top: 10px;
  border-top: 6px solid var(--text);
  font-size: clamp(1.3rem, 2.6vw, 1.7rem);
}

.countdown-grid {
  display: flex;
  gap: clamp(18px, 3vw, 30px);
  justify-content: flex-start;
  flex-wrap: wrap;
  margin-top: 16px;
}

/* Split-flap departure board. Each digit is its own module: two
   halves split across the middle, and a changed digit drops its top
   leaf away to show the new numeral behind it, then swings the new
   bottom leaf up over the old one — the Solari boards these stations
   used to run on. */
.count-tile {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
}

.count-num {
  --flap-h: clamp(38px, 7vw, 54px);
  --flap-w: calc(var(--flap-h) * 0.7);
  --flap-fall: 130ms;
  display: flex;
  gap: 2px;
  justify-content: flex-start;
}

.flap {
  position: relative;
  width: var(--flap-w);
  height: var(--flap-h);
  perspective: 320px;
  box-shadow: var(--shadow);
  border-radius: 2px;
}

/* the two halves, with the hairline of the split between them */
.flap-half {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(50% - 1px);
  overflow: hidden;
  background: var(--sign-bg);
}
.flap-upper { top: 0; border-radius: 2px 2px 0 0; }
.flap-lower { bottom: 0; border-radius: 0 0 2px 2px; }
/* the lower half catches a touch less light, the way a hinged card does */
.flap-lower { background: linear-gradient(var(--sign-bg), #1d1d1d); }

/* the numeral is drawn at the full height of the module inside each
   half, so the two halves show the top and bottom of one glyph */
.flap-half b {
  position: absolute;
  left: 0;
  width: 100%;
  height: var(--flap-h);
  line-height: var(--flap-h);
  text-align: center;
  /* the same mono the quest codes are set in: a fixed-width face is
     what these boards used, and every numeral sits the same */
  font-family: var(--mono);
  font-weight: 700;
  font-size: calc(var(--flap-h) * 0.58);
  color: var(--sign-ink);
  font-variant-numeric: tabular-nums;
}
.flap-upper b { top: 0; }
.flap-lower b { bottom: 0; }

/* the leaves that move: the old top falls, then the new bottom rises */
.flap-leaf {
  z-index: 2;
  backface-visibility: hidden;
  transform: rotateX(0deg);
}
.flap-leaf-a { transform-origin: 50% 100%; }
.flap-leaf-b { transform-origin: 50% 0%; transform: rotateX(90deg); }

.flap.is-flipping .flap-leaf-a { animation: flap-fall var(--flap-fall) ease-in forwards; }
.flap.is-flipping .flap-leaf-b { animation: flap-rise var(--flap-fall) var(--flap-fall) ease-out forwards; }
@keyframes flap-fall { to { transform: rotateX(-90deg); } }
@keyframes flap-rise { from { transform: rotateX(90deg); } to { transform: rotateX(0deg); } }

/* a shadow sweeps the card as it turns, so the fold reads as depth */
.flap-leaf::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #000000;
  opacity: 0;
  pointer-events: none;
}
.flap.is-flipping .flap-leaf-a::after { animation: flap-shade-in var(--flap-fall) ease-in forwards; }
.flap.is-flipping .flap-leaf-b::after { animation: flap-shade-out var(--flap-fall) var(--flap-fall) ease-out forwards; }
@keyframes flap-shade-in { from { opacity: 0; } to { opacity: 0.55; } }
@keyframes flap-shade-out { from { opacity: 0.55; } to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .flap.is-flipping .flap-leaf-a,
  .flap.is-flipping .flap-leaf-b,
  .flap.is-flipping .flap-leaf::after { animation: none; }
}

.count-label {
  display: block;
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 800;
}

.countdown-note {
  margin: 14px 0 0;
  color: var(--muted);
  font-size: 0.9rem;
  max-width: 560px;
  line-height: 1.5;
  text-wrap: pretty;
}

.countdown-message {
  color: var(--accent);
  font-size: clamp(1.1rem, 3vw, 1.5rem);
  font-weight: 850;
  letter-spacing: -0.03em;
  padding: 18px 0 6px;
}

/* ---------------- Prose (info & welfare pages) ---------------- */

/* The measure belongs to the running text, not to the section. It was
   on .prose, so the heavy black rule over a heading stopped at 780px
   while the rules on the sections either side ran the full width, and
   the page had a ragged right edge that read as a rendering fault. */
.prose { line-height: 1.65; }
/* About sixty-five characters. It was set in ch, which is the width of
   a zero — far wider than Helvetica's average lowercase letter — so a
   measure meant to be sixty-two ran to seventy-three. */
.prose > p,
.prose > ul,
.prose > ol,
.prose > .section-sub { max-width: 31em; }

.prose > p.lead { max-width: 30em; }

.prose h2 { margin: 0 0 12px; }

.prose .chip-row { margin-top: 6px; }

.prose h3 {
  margin: 20px 0 8px;
  font-size: var(--t-lede);
  letter-spacing: -0.02em;
}

.prose p { margin: 0 0 12px; }
.prose p:last-child { margin-bottom: 0; }

.prose ul, .prose ol { margin: 0 0 12px; padding-left: 22px; }
.prose li { margin: 6px 0; }
.prose li::marker { color: var(--accent); font-weight: 800; }

/* A ruled list in place of markers. At 5px the coloured dots read as
   specks of dirt rather than as route discs, and a hairline says the
   same thing in the language the rest of the page is written in. */
.prose ul.ruled-list, .prose ol.ruled-list { padding-left: 0; }
.prose .ruled-list li { margin: 0; }
.prose .ruled-list li::marker { content: none; }

.prose a {
  color: var(--text);
  font-weight: 700;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.prose a:hover { text-decoration-thickness: 2px; }

.prose .muted { color: var(--muted); }

/* Warning / info notices */
.notice {
  border: 1px solid var(--notice-border);
  background: var(--notice-bg);
  border-radius: 2px;
  padding: 12px 16px;
  margin: 14px 0;
  line-height: 1.55;
}

.notice-info {
  border-color: var(--accent-soft-border);
  background: var(--accent-soft);
}

/* Schedule timeline */
.timeline {
  list-style: none;
  padding: 0;
  margin: 0;
}

.timeline li {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 14px;
  padding: 11px 0;
  border-bottom: 1px dashed var(--border);
}

.timeline li:last-child { border-bottom: none; }

/* times wear little station signs */
.timeline time {
  justify-self: start;
  align-self: start;
  background: var(--sign-bg);
  color: var(--sign-ink);
  border-radius: 2px;
  padding: 3px 9px;
  font-weight: 800;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Tables */
.table-scroll { overflow-x: auto; }

table.nice {
  border-collapse: collapse;
  width: 100%;
  min-width: 520px;
}

table.nice th {
  text-align: left;
  color: var(--muted);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 8px 12px;
  border-bottom: 2px solid var(--border);
}

table.nice td {
  padding: 11px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  line-height: 1.5;
}

table.nice tr:last-child td { border-bottom: none; }

table.nice .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* FAQ */
.faq details {
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  margin: 8px 0;
  overflow: hidden;
}

.faq summary {
  padding: 13px 16px;
  cursor: pointer;
  font-weight: 750;
}

.faq summary:hover { color: var(--accent); }

.faq .faq-a {
  padding: 0 16px 13px;
  color: var(--muted);
  line-height: 1.6;
}
.faq details[open] .faq-a { animation: t-rise var(--duration-fast) var(--ease-smooth-out) both; }
.faq summary { transition: color var(--duration-quick) var(--ease-smooth-out); }
@media (prefers-reduced-motion: reduce) { .faq details[open] .faq-a { animation: none; } }

/* Contact cards (welfare page) */
.contact-card {
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 16px;
  background: var(--surface);
  box-shadow: var(--shadow);
}

.contact-card.danger {
  border-color: var(--danger-border);
  background: var(--danger-bg);
}

.contact-num {
  display: inline-block;
  margin-top: 6px;
  font-size: 1.55rem;
  font-weight: 900;
  letter-spacing: -0.03em;
  color: var(--text);
  text-decoration: none;
}

.contact-num:hover { color: var(--accent); }

.contact-card p { margin: 8px 0 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }

/* Escalation ladder (welfare page) */
.ladder { display: grid; gap: 10px; }

.ladder-step {
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 13px 15px;
  background: var(--surface);
  border-left: 5px solid var(--border);
  line-height: 1.55;
}

.ladder-step.low { border-left-color: var(--line-lime); }
.ladder-step.moderate { border-left-color: var(--line-yellow); }
.ladder-step.high { border-left-color: var(--line-red); }

.ladder-step strong { display: block; margin-bottom: 4px; }

/* ---------------- Forms (submit, registration) ---------------- */

.sub-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: 12px;
}

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

/* quest codes: the tab's reference, set in mono wherever they appear */
.code {
  display: inline-block;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.8rem;
  line-height: 1.5;
  padding: 0 6px;
  border-radius: 2px;
  background: var(--sign-bg);
  color: var(--sign-ink);
  white-space: nowrap;
}
.code.code-muted { background: var(--border); color: var(--text); }

/* quest text rendered from markdown */
.md p { margin: 0 0 0.7em; }
.md p:last-child, .md ul:last-child { margin-bottom: 0; }
.md ul { margin: 0 0 0.7em; padding-left: 1.2em; }
.md li { margin: 0 0 0.25em; }
.md code { font-family: var(--mono); font-size: 0.9em; padding: 0 3px; background: var(--page-bg); border: 1px solid var(--border); border-radius: 2px; }
.md a { color: var(--accent-dark); }
.md u { text-decoration-thickness: 2px; text-underline-offset: 2px; }

.sub-field { display: grid; gap: 6px; margin: 0 0 12px; }

.sub-field > span {
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.sub-field input,
.sub-field textarea {
  width: 100%;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  padding: 11px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  transition: border-color var(--duration-quick) var(--ease-smooth-out);
}

.sub-field input:focus-visible,
.sub-field textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.sub-field input[type="file"] { padding: 9px; background: var(--page-bg); }

.sub-hint { color: var(--muted); font-size: 0.88rem; margin: 10px 0 0; line-height: 1.5; }
.sub-error { color: var(--line-red); font-weight: 650; margin: 10px 0 0; min-height: 1.4em; }
.sub-status { margin: 10px 0 0; min-height: 1.4em; font-weight: 650; }
.sub-status.is-error { color: var(--line-red); }

.sub-actions { display: flex; gap: 14px; align-items: center; margin-top: 10px; }

.sub-progress {
  flex: 1;
  max-width: 300px;
  height: 10px;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  background: var(--surface);
  overflow: hidden;
}
.sub-progress > span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width var(--duration-fast) var(--ease-smooth-out);
}

/* a hidden checkbox driving an O/I rocker, with its label alongside */
.sub-check {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 0;
  cursor: pointer;
  font-weight: 650;
}
.sub-check input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sub-check a { color: var(--accent); font-weight: 700; }

/* status pills: pending, accepted, rejected */
.pill {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 2px;
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.pill.pending { background: var(--line-yellow); color: #111111; }
.pill.accepted { background: var(--accent-solid); color: var(--on-accent); }
.pill.rejected { background: var(--line-red); color: #ffffff; }

/* ---------------- Pop-up notifications (toasts) ----------------
   They sit under the top bar, not over the bottom of the page: a
   quest sheet, the tab bar and the camera button all live down there
   and a pop-up over them hid what had just been ticked. */

.toast-stack {
  position: fixed;
  left: 50%;
  top: calc(var(--nav-h, 60px) + 10px);
  transform: translateX(-50%);
  display: grid;
  gap: 8px;
  justify-items: center;
  z-index: 1500;
  width: max-content;
  max-width: calc(100vw - 24px);
  pointer-events: none;
}
html.is-pwa .toast-stack { top: calc(63px + env(safe-area-inset-top, 0px)); }
/* the full screen map hides the site's chrome, so there is no bar to
   sit under */
html.ql-map-full .toast-stack { top: calc(12px + env(safe-area-inset-top, 0px)); }

.toast {
  background: rgba(12, 15, 20, 0.94);
  color: #fff;
  border-radius: 2px;
  padding: 10px 14px;
  font-size: 0.9rem;
  font-weight: 650;
  line-height: 1.4;
  box-shadow: 2px 2px 0 rgba(12, 15, 20, 0.25);
  max-width: 100%;
  opacity: 0;
  /* the resting state is also where it leaves: in from above, back up
     as it fades out */
  transform: translateY(calc(-1 * var(--toast-distance))) scale(var(--toast-scale));
  filter: blur(var(--toast-blur));
  will-change: transform, opacity, filter;
  transition:
    opacity   var(--toast-close) var(--ease-smooth-out),
    transform var(--toast-close) var(--ease-smooth-out),
    filter    var(--toast-close) var(--ease-smooth-out);
}

.toast.is-open {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity   var(--toast-open) var(--ease-smooth-out),
    transform var(--toast-open) var(--ease-smooth-out),
    filter    var(--toast-open) var(--ease-smooth-out);
}

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

html[data-theme="dark"] .toast {
  background: rgba(233, 237, 240, 0.97);
  color: #10151a;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
}

/* ---------------- Footer ---------------- */

/* the footer is the other end of the chrome, and takes the same ground
   and the same hairline as the bar at the top */
.site-footer {
  border-top: 1px solid var(--bar-edge);
  margin-top: 48px;
  background: var(--bar-bg);
  color: var(--bar-muted);
  font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif; /* the signage face */
}

.site-footer-inner {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  padding: 26px 0 34px;
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: start;
  justify-content: space-between;
  color: var(--bar-muted);
  font-size: 0.92rem;
  line-height: 1.6;
}

.site-footer strong { color: var(--bar-ink); }

.site-footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

/* the facts, where the home page's footer used to repeat the nav */
.site-footer-facts {
  display: grid;
  gap: 3px;
  text-align: right;
}

@media (max-width: 700px) { .site-footer-facts { text-align: left; } }

.site-footer a {
  color: var(--bar-ink);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.site-footer a:hover { text-decoration-thickness: 2px; }

/* ---------------- PWA overscroll ---------------- */
/* Installed app: pin the document and make <body> the scroll container.
   The content keeps its native rubber-band bounce inside the body
   scroller, while the app frame (fixed top bar + bottom tab bar, which
   live outside the scrolling layer) never moves with it. */

html.is-pwa {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

html.is-pwa body {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior-y: contain; /* keep the bounce, stop chaining/pull-to-refresh */
  overscroll-behavior-x: none; /* no pull-from-edge back/forward swipe navigation */
}

/* No long-press link previews (peek and pop) inside the app; links act
   like native controls, not draggable web links */
html.is-pwa a {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* While a remembered scroll position is being applied the content stays
   invisible (the bars stay), so the top of the page never flashes into
   view mid-switch. visibility keeps layout, letting the jump land; the
   boot script adds the class and site.js reveals on restore or deadline. */
html.is-pwa.scroll-restoring main,
html.is-pwa.scroll-restoring #root { visibility: hidden; }

/* The quest page builds its list with a deferred script after first
   paint; keep its content veiled until the list exists so tab switches
   show one complete paint instead of a half-empty flash. The head script
   adds the class; the quest script (or a 900ms safety) removes it. */
html.ql-pending main { visibility: hidden; }

/* ---------------- PWA launch splash ---------------- */
/* Painted from the very first frame on a cold app launch (the head boot
   script flags html.show-splash before render) and dismissed by site.js
   as soon as the page is ready. Signage black in both themes. */

.app-splash { display: none; }

html.show-splash .app-splash {
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: #131313;
  opacity: 1;
  transition: opacity var(--duration-fast) var(--ease-smooth-out);
}

.app-splash span {
  color: #fff;
  font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif;
  font-weight: 900;
  font-size: clamp(2.6rem, 13vw, 4.5rem);
  letter-spacing: -0.04em;
}

.app-splash.is-leaving { opacity: 0; pointer-events: none; }

/* App and phone browsers: no page pinch/double-tap zoom (the Leaflet map
   keeps its own pinch handling, so map zoom still works) */
html.is-pwa,
html.is-pwa body { touch-action: pan-x pan-y; }
@media (max-width: 840px) {
  html:not(.is-pwa),
  html:not(.is-pwa) body { touch-action: pan-x pan-y; }
  /* no pull-from-edge back/forward swipe on phones either (the app
     block below says the same for the installed app) */
  html:not(.is-pwa),
  html:not(.is-pwa) body { overscroll-behavior-x: none; }
}

/* ---------------- PWA app-mode chrome ---------------- */
/* Installed app: the web footer is redundant (tab bar + info page cover it) */
html.is-pwa .site-footer { display: none; }

/* The bottom bar already covers these destinations in the app, so the
   links beside the button go — but not the button. This rule was
   written when the row held nothing but destinations; the day it
   gained Sign up, which the bar does not carry, it started hiding the
   only way to register from every phone. */
html.is-pwa .hero-also { display: none; }

/* Floating Submit a quest button: a plain link to /submit/, on every
   page but that one and on every device. */
/* clear of the fixed footer: the two were both pinned bottom-right
   and the button sat over the footer links */
.fab-wrap {
  display: block;
  position: fixed;
  right: 14px;
  bottom: calc(var(--fab-lift, 0px) + 16px + env(safe-area-inset-bottom, 0px));
  z-index: 1140;
}
html.is-pwa .fab-wrap { bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
@media (min-width: 841px) {
  .fab-wrap { right: calc(22px + env(safe-area-inset-right, 0px)); bottom: calc(var(--fab-lift, 0px) + 18px); }
}
/* faded whenever the top bar is hidden; solid again together with it */
.fab-wrap { transition: opacity var(--chrome-fade); }
/* The button sits above the settings menu — the menu is in the top
   bar's stacking context, and the button is above that bar — so when
   the panel reaches the bottom of a short screen the button would
   cover its last row. While the menu is open the button steps back
   with the veil, on the menu's own clock, and a tap where it was is a
   tap outside the menu, which closes it. */
html.settings-open .fab-wrap,
html.settings-closing .fab-wrap {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dropdown-open-dur) var(--ease-smooth-out);
}
html.nav-up .fab-wrap { opacity: 0.4; }
/* Not on a desktop that is not running the app layout: the bar across
   the top already carries Submit a Quest as one of its links, and a
   second way to the same page floating in the corner is one more thing
   in front of the page. A handheld keeps it — there the links are
   behind the burger, or behind the app's bottom bar, and the corner is
   where a thumb already is. */
@media (min-width: 841px) {
  html:not(.is-pwa) .fab-wrap { display: none; }
}
/* nudged in from the corner — and from the safe area, which on a
   phone turned sideways is a curve and sometimes the notch, so a bare
   20px puts the button somewhere nobody can see or press */
/* --fab-lift (site-legacy.js) is how much of the fixed footer is
   actually showing, which is 0 for all but the last screen of a page.
   It used to be the footer's whole height: the footer is only revealed
   at the end of the scroll, so the button sat a phone's-worth above the
   bottom of the screen with nothing under it. */
.fab-wrap { right: calc(20px + env(safe-area-inset-right, 0px)); bottom: calc(var(--fab-lift, 0px) + 18px + env(safe-area-inset-bottom, 0px)); }
html.is-pwa .fab-wrap { bottom: calc(70px + env(safe-area-inset-bottom, 0px)); }

/* ---------------- While the keyboard is up ----------------
   A phone's keyboard takes the bottom of the screen. The app's bar has
   nothing to do while somebody is typing and was left sitting in the
   gap between the keyboard and the field, so it leaves. --kb-inset
   (site-legacy.js) is how much of the layout viewport the keyboard
   covers, which is 0 wherever the browser resized the view instead of
   laying the keyboard over it. */
html.kb-up .tab-bar {
  transform: translateY(100%);
  opacity: 0;
  pointer-events: none;
}
html.kb-up .fab-wrap,
html.is-pwa.kb-up .fab-wrap { bottom: calc(var(--kb-inset, 0px) + 12px); }

.fab-btn {
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  padding: 0;
  border: none;
  border-radius: 2px;
  background: var(--accent-solid);
  color: var(--on-accent);
  transition: transform var(--duration-quick) var(--ease-smooth-out);
}
.fab-btn:active { transform: scale(0.93); }
.fab-btn svg {
  width: 26px;
  height: 26px;
}

/* ---------------- Install nudge (mobile web) ---------------- */
.install-nudge {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 1400;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  box-shadow: var(--shadow-hover);
  padding: 14px 16px;
  opacity: 0;
  transform: translateY(var(--distance-medium));
  filter: blur(var(--blur-small));
  transition:
    opacity   var(--duration-medium) var(--ease-smooth-out),
    transform var(--duration-medium) var(--ease-smooth-out),
    filter    var(--duration-medium) var(--ease-smooth-out);
}
.install-nudge.is-open {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition:
    opacity   var(--duration-slow) var(--ease-smooth-out),
    transform var(--duration-slow) var(--ease-smooth-out),
    filter    var(--duration-slow) var(--ease-smooth-out);
}
.install-nudge strong { display: block; margin-bottom: 4px; }
.install-nudge p { margin: 0 0 10px; color: var(--muted); font-size: 0.92rem; line-height: 1.5; }
.install-nudge-row { display: flex; gap: 8px; justify-content: flex-end; }
.install-nudge-row button {
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 700;
  font-size: 0.9rem;
  padding: 8px 12px;
  cursor: pointer;
}
.install-nudge-row .install-nudge-later {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent);
}
@media (prefers-reduced-motion: reduce) { .install-nudge { transition: none !important; } }

/* ---------------- PWA bottom tab bar ---------------- */
/* Shown only when the site runs as an installed app (site.js adds
   html.is-pwa in standalone display mode) — app-style reachability
   navigation like Instagram's bottom bar. */

.tab-bar { display: none; }

@media (max-width: 840px) {
  html.is-pwa .tab-bar {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    position: fixed;
    left: 0;
    right: 0;
    /* a pixel below the edge: iOS rounds the viewport height so that a
       bar sat exactly at 0 showed a hairline of page beneath it */
    bottom: -1px;
    z-index: 1150;
    background: var(--bar-bg);
    border-top: 1px solid var(--bar-edge);
    /* The 12px above the icons was the five-route stripe's room: it was
       painted over the top 10 of it, so the icons sat two pixels under
       a rule. With a hairline there instead the same padding read as a
       band of nothing, so it comes back to match the space below. */
    padding: 8px 4px calc(8px + env(safe-area-inset-bottom, 0px));
    font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", sans-serif; /* the signage face */
    transition: opacity var(--duration-fast) var(--ease-smooth-out);
  }
  html.is-pwa .tab-bar {
    transition: opacity var(--chrome-fade),
      transform var(--duration-fast) var(--ease-smooth-out);
  }
  html.is-pwa.nav-up .tab-bar { opacity: 0.45; }

  /* Icon-only tabs: the filled icon, lit in that stop's line colour,
     marks the active page — no labels, for a slim bar */
  html.is-pwa .tab-bar a {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5px 2px;
    text-decoration: none;
    color: var(--bar-muted);
    -webkit-tap-highlight-color: transparent;
    transition: color var(--duration-fast) var(--ease-smooth-out);
  }
  html.is-pwa .tab-bar .tab-home { --tab-line: var(--line-green); }
  html.is-pwa .tab-bar .tab-quests { --tab-line: var(--line-blue); }
  html.is-pwa .tab-bar .tab-info { --tab-line: var(--line-orange); }
  html.is-pwa .tab-bar .tab-welfare { --tab-line: var(--line-red); }
  html.is-pwa .tab-bar .tab-tab { --tab-line: var(--tab-yellow); }
  html.is-pwa .tab-bar .tab-submit { --tab-line: var(--line-purple); }

  /* labels stay for screen readers only */
  html.is-pwa .tab-bar a span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }

  html.is-pwa .tab-bar svg { width: 27px; height: 27px; }
  html.is-pwa .tab-bar .icon-fill { display: none; }
  html.is-pwa .tab-bar a.is-active svg:not(.icon-fill) { display: none; }
  html.is-pwa .tab-bar a.is-active .icon-fill { display: block; }
  html.is-pwa .tab-bar a.is-active { color: var(--tab-line, var(--bar-ink)); }
  html.is-pwa .tab-bar a:active { opacity: 0.65; }

  /* Fix the top bar to the viewport (outside the bouncing scroller) so it
     stays put while content rubber-bands beneath it. It slides away on
     downward scroll and returns on a fast upward flick (site.js drives
     html.nav-up); the Season 1 archive keeps its bar put. */
  html.is-pwa .site-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    padding-top: env(safe-area-inset-top, 0px);
    transition: transform var(--chrome-slide);
  }
  /* Bar drops in only when the chrome was hidden on the page you left
     (the boot script sets html.nav-enter from a sessionStorage flag) */
  html.is-pwa.nav-enter .site-nav { animation: nav-drop var(--duration-fast) var(--ease-smooth-out); }
  @keyframes nav-drop {
    from { transform: translateY(calc(-100% + 1px)); }
    to { transform: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    html.is-pwa.nav-enter .site-nav { animation: none; }
  }
  /* mobile web hides the bar the same way the app does */
  html:not(.is-pwa) .site-nav { transition: transform var(--chrome-slide); }
  /* the extra 8px takes the route stripe out with the bar */
  html.nav-up .site-nav:not(.site-nav--static) { transform: translateY(calc(-100% - 8px)); }
  html.is-pwa .site-nav-inner { height: 52px; padding-top: 0; padding-bottom: 0; }

  /* Keep page content and pop-ups clear of the fixed bars */
  html.is-pwa body {
    padding-top: calc(53px + env(safe-area-inset-top, 0px));
    padding-bottom: calc(52px + env(safe-area-inset-bottom, 0px));
    /* The strip the top bar covers — the notch included — painted by
       the page's own background rather than waiting for the bar to lay
       out. On iPhone a page switch shows the new document's background
       a frame or two before its fixed bars are drawn, and that frame
       was the paper colour flashing behind the cutout before the black
       arrived. A background is up on the first paint, so there is no
       frame where the strip is the wrong colour. It is the body's own
       background layer, so nothing is painted over the page and the
       bar still draws itself on top as it always did. The height is
       the body's top padding, which exists for exactly this bar. */
    background-image: linear-gradient(var(--bar-bg), var(--bar-bg));
    background-repeat: no-repeat;
    background-size: 100% calc(53px + env(safe-area-inset-top, 0px));
  }

  /* What covers the cutout once the bar has slid away. The bar itself
     covers it while it is there (its padding-top IS the inset), and the
     strip painted on the body's background is only there to stop a
     paper flash on a page switch — a background sits behind the page's
     own content, so it covers nothing, and text scrolled straight up
     under the notch. This is the band that does cover it: the bar's
     ground for the height of the inset, with the bar's hairline under
     it, so the cutout still reads as separate from the page moving
     below. On a screen with no inset it is the hairline alone.
     `html::after` is the free slot; `body::after` is the quest page's
     sheet dim and must not be taken. */
  html.is-pwa::after {
    content: '';
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: env(safe-area-inset-top, 0px);
    z-index: 1099;   /* under the bar, over the page */
    background: var(--bar-bg);
    border-bottom: 1px solid var(--bar-edge);
    opacity: 0;
    transition: opacity var(--chrome-fade);
    pointer-events: none;
  }
  html.is-pwa.nav-up::after { opacity: 1; }

  /* Season 1 archive page: its retro header/tab strip would stick behind
     the fixed app bar, so let them scroll normally in app mode */
  html.is-pwa .header,
  html.is-pwa .nav { position: static; }

  /* The bottom bar handles navigation — simplify the top bar into a
     centred app title (no burger, no dropdown menu); theme toggle pins right */
  html.is-pwa .site-nav-toggle { display: none; }
  html.is-pwa .site-menu { display: none; }
  html.is-pwa .site-nav-inner { justify-content: center; }
  html.is-pwa .site-brand { margin-right: 0; }
  html.is-pwa .settings-toggle {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
  }
  /* The app bar centres the brand as its title, so the bell takes the
     empty left end rather than the gear's shoulder: at phone widths the
     title runs the width of the bar and would slide under a button
     parked beside the gear. */
  html.is-pwa .bell-toggle {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
  }

  /* The bar is the page's title bar. The page's name takes the brand's
     place in it, and the page stops drawing the name a second time at
     the top of its own content: on most pages that is the h1 alone, and
     on the four whose band held nothing else it is the band. The home
     page carries neither class, so its bar still reads SCS.2 Legends of
     the City and its board is untouched.

     Both rules key off html.is-pwa rather than off a class added once,
     so turning App layout off in the settings menu puts the brand and
     every page title straight back with no reload. */
  html.is-pwa .site-brand.has-page-title .site-brand-badge,
  html.is-pwa .site-brand.has-page-title .site-brand-name { display: none; }
  html.is-pwa .site-brand.has-page-title .site-brand-page { display: block; }
  html.is-pwa .app-titled { display: none; }

  /* A long name must not run under the bell or the gear, which are both
     40px and sit hard against the ends: it gives up its last words
     rather than sliding beneath one. */
  html.is-pwa .site-brand.has-page-title {
    max-width: calc(100% - 108px);
    overflow: hidden;
  }
  html.is-pwa .site-brand-page {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ---------------- PWA / notched devices ---------------- */
/* Keeps content clear of the notch and home indicator when the site is
   installed to the home screen (viewport-fit=cover). */

@supports (padding: max(0px)) {
  .site-nav-inner,
  .site-footer-inner,
  .page-shell {
    padding-left: max(0px, env(safe-area-inset-left));
    padding-right: max(0px, env(safe-area-inset-right));
  }

  .site-footer { padding-bottom: env(safe-area-inset-bottom); }
}

/* ---------------- Small screens ---------------- */

@media (max-width: 930px) {
  .hero { display: block; }
}

@media (max-width: 560px) {
  .page-shell { width: min(100% - 32px, 1180px); padding-top: 18px; }
  .hero { padding: 16px 0 6px; }
  .hero h1.sign-bar { padding: 14px 16px; gap: 14px; }
  .hero-copy.sign-bar { padding: 12px 16px; }
}

/* ---------------- Pull to refresh (phones) ----------------
   The page itself follows the finger down on a rubber band and eases
   back when let go. The arrow is part of the page: it sits in the
   strip under the top bar, above the first content, so the pull carries
   it out from under the bar and it is never drawn over content. It
   flips up once the pull is far enough, and a bar runs under the top
   bar while the page reloads. The native bounce stays for the bottom
   of the page; at the top the script claims the gesture first. */
html.has-pull body { overscroll-behavior-y: contain; }
/* the page is the thing that moves: positioned so the arrow rides with
   it, opaque, its own stacking context so the arrow stays beneath
   everything in it */
html.has-pull main[data-pull] { position: relative; z-index: 1; background: var(--page-bg); }
/* the transform is only on while a pull is in progress (it would
   otherwise pin fixed layers inside the page, like the fullscreen map) */
html.is-pulling main[data-pull] { transform: translateY(var(--pull, 0px)); }
html.is-pull-settling main[data-pull] { transition: transform var(--duration-fast) var(--ease-smooth-out); }
.ptr {
  position: absolute;
  left: 50%;
  top: -44px; /* wholly in the strip the top bar covers, until the page moves */
  z-index: -1; /* beneath the page's content, whatever happens */
  pointer-events: none;
  transform: translateX(-50%);
}
.ptr.is-loading { opacity: 0; transition: opacity var(--duration-quick) var(--ease-smooth-out); }
.ptr-disc {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  color: var(--text);
}
.ptr-disc svg { width: 28px; height: 28px; transition: transform var(--duration-fast) var(--ease-smooth-out); }
.ptr.is-armed .ptr-disc svg { transform: rotate(180deg); }
.ptr-bar {
  position: fixed;
  left: 0;
  right: 0;
  top: calc(var(--nav-h, 60px));
  height: 4px;
  z-index: 1200;
  background: var(--border);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-quick) var(--ease-smooth-out);
}
html.is-pwa .ptr-bar { top: calc(53px + env(safe-area-inset-top, 0px)); }
.ptr-bar span { display: block; height: 100%; width: 0; background: var(--accent); }
.ptr-bar.is-loading { opacity: 1; }
.ptr-bar.is-loading span { animation: ptr-fill 650ms linear forwards; }
@keyframes ptr-fill { from { width: 0; } to { width: 100%; } }

/* "New quests" drops in under the top bar; a tap reloads */
.update-banner {
  position: fixed;
  left: 50%;
  top: calc(var(--nav-h, 60px) + 10px);
  z-index: 1190;
  transform: translate(-50%, calc(-100% - 20px));
  transition: transform var(--duration-medium) var(--ease-smooth-out);
  background: var(--accent-solid);
  color: var(--on-accent);
  border: 0;
  border-radius: 2px;
  box-shadow: var(--shadow);
  padding: 0;
  font: inherit;
  font-weight: 700;
  font-size: 0.92rem;
  white-space: nowrap;
  display: inline-flex;
  align-items: stretch;
}
.update-banner button {
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  font-weight: 700;
  padding: 10px 14px;
  display: inline-flex;
  gap: 10px;
  align-items: center;
  cursor: pointer;
}
.update-banner button + button { border-left: 1px solid rgba(255, 255, 255, 0.35); padding: 10px 12px; font-size: 1.1rem; line-height: 1; }
html.is-pwa .update-banner { top: calc(63px + env(safe-area-inset-top, 0px)); }
.update-banner[hidden] { display: none !important; }
.update-banner.is-shown { transform: translate(-50%, 0); }
.update-banner svg { width: 16px; height: 16px; }
@media (prefers-reduced-motion: reduce) {
  .ptr, .ptr-disc svg, .update-banner { transition: none; }
}

/* ---------------- Urgent message ----------------
   An urgent message opens over whatever page it lands on: one card on
   dimmed ground, above the bars, closed by the button, the ground
   around it, Escape or the system back. */
.urgent-overlay {
  position: fixed;
  inset: 0;
  z-index: 1450;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(10, 12, 14, 0.62);
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-smooth-out);
}

.urgent-overlay.is-open { opacity: 1; }

.urgent-card {
  width: min(420px, 100%);
  max-height: calc(100% - 24px);
  overflow: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 2px;
  box-shadow: var(--shadow);
  transform: translateY(var(--distance-medium));
  transition: transform var(--duration-fast) var(--ease-smooth-out);
}

.urgent-overlay.is-open .urgent-card { transform: none; }

.urgent-head {
  margin: 0;
  padding: 10px 16px;
  background: var(--sign-bg);
  color: var(--sign-ink);
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.urgent-body { padding: 16px; }

.urgent-title {
  margin: 0 0 10px;
  font-size: 1.08rem;
  font-weight: 850;
  letter-spacing: -0.02em;
}

.urgent-text { line-height: 1.6; }
.urgent-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }

@media (prefers-reduced-motion: reduce) {
  .urgent-overlay, .urgent-card { transition: none; }
}

/* A quest embedded in a message ([[quest:SQ40]] on its own line): the
   quest's chip and name with the two ways in. Built by scs.messageBody,
   shown on the messages page and inside the urgent pop-up. */
.mq {
  margin: 12px 0;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--page-bg);
}

.mq-head {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.mq-name { font-weight: 700; }
.mq-points { margin-left: auto; color: var(--muted); font-size: 0.85rem; white-space: nowrap; }
.mq-meta { margin: 6px 0 0; color: var(--muted); font-size: 0.88rem; }
.mq-actions { margin: 12px 0 0; display: flex; gap: 10px; flex-wrap: wrap; }

/* the card sits inside a markdown body, whose link colour must not
   reach the buttons */
.md .mq-actions a { color: var(--text); }
.md .mq-actions a.btn-accent { color: var(--on-accent); }

/* ---------------- Lightbox ----------------
   Proof photos and videos preview over the page instead of opening
   the raw file in a new tab. Above the sheet and the fullscreen map.
   Layers, bottom up: the bar (1), the photo (2, so a zoomed photo may
   spill over the bar), the message sheet (3), and the close button
   and slide arrows (4), which nothing covers. */
.lb-overlay {
  --lb-bar-h: var(--nav-h, 64px);
  --lb-bottom: 0px;
  position: fixed;
  inset: 0;
  z-index: 1400;
  background: rgba(10, 12, 14, 0.88);
  animation: lb-in var(--duration-quick) var(--ease-smooth-out);
  transition: background var(--duration-medium) var(--ease-smooth-out);
}
html.is-pwa .lb-overlay { --lb-bar-h: calc(52px + env(safe-area-inset-top, 0px)); }
.lb-overlay.is-leaving { opacity: 0; transition: opacity 240ms var(--ease-smooth-out); }
@keyframes lb-in { from { opacity: 0; } }

/* The photo's area is the screen above the sheet, and the photo is
   centred in it with the bar's height kept clear at both ends, so on
   a page with no sheet it sits at the centre of the screen rather
   than the centre of the strip under the bar. The area follows the
   sheet: --lb-bottom is what the sheet is showing. */
.lb-frame {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: var(--lb-bottom);
  z-index: 2;
  margin: 0;
  padding: var(--lb-bar-h) 16px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  /* the area itself takes no taps: a tap beside the photo reaches the
     backdrop (which closes) and a tap on the bar reaches the bar */
  pointer-events: none;
  transition:
    transform 240ms cubic-bezier(0.22, 1, 0.36, 1),
    bottom 300ms cubic-bezier(0.22, 1, 0.36, 1),
    padding 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lb-frame > * { pointer-events: auto; }
.lb-overlay.has-sheet .lb-frame { padding-bottom: 12px; }
/* a finger is moving it: no easing between moves */
.lb-frame.is-dragging { transition: none; }
.lb-media {
  max-width: min(100%, 1100px);
  max-height: 100%;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14);
  animation: lb-media-in var(--duration-fast) var(--ease-smooth-out);
  background: #000;
}
/* photos zoom in place: the browser's own gestures stand down so
   pinch, wheel and drag reach the script; videos keep theirs */
img.lb-media { touch-action: none; cursor: zoom-in; }
img.lb-media.is-zoomed { cursor: grab; }
/* while the open/close morph drives transform, the entry animation
   must not fight it */
.lb-media.lb-morphing { animation: none; }
@keyframes lb-media-in { from { opacity: 0; transform: translateY(6px); } }

/* One tap on the photo: alone on a black screen, edge to edge, the
   bar and the sheet gone until the next tap. */
.lb-overlay.is-immersive { background: #000; }
.lb-overlay.is-immersive .lb-frame { bottom: 0; padding: 0; }
.lb-overlay.is-immersive .lb-media { width: 100%; height: 100%; max-width: 100%; object-fit: contain; border-radius: 0; box-shadow: none; }
.lb-overlay.is-immersive .lb-bar,
.lb-overlay.is-immersive .lb-top { transform: translateY(-110%) !important; }
.lb-overlay.is-immersive .lb-top { pointer-events: none; }
.lb-overlay.is-immersive .lb-nav { opacity: 0; pointer-events: none; }
.lb-overlay.is-immersive .lb-sheet { pointer-events: none; }

/* The lightbox's own top bar: the same black sign as the app bar, the
   same height, the same stripe along its bottom edge, so the close
   button sits exactly where the gear sits on the page beneath. The
   close button lives in .lb-top, a transparent twin of the bar laid
   over everything, so a zoomed photo can cover the bar's black but
   never the button. Both slide off the top as the photo is swiped
   away and on arrival slide in. */
.lb-bar,
.lb-top {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--lb-bar-h);
  display: flex;
  align-items: stretch;
  animation: lb-bar-in 260ms cubic-bezier(0.2, 0.7, 0.3, 1);
  transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lb-bar {
  z-index: 1;
  background: var(--bar-bg);
  color: var(--bar-ink);
}
.lb-top { z-index: 4; pointer-events: none; }
.lb-top .lb-close { pointer-events: auto; }
html.is-pwa .lb-bar,
html.is-pwa .lb-top { padding-top: env(safe-area-inset-top, 0px); }
.lb-overlay.is-chrome-dragging .lb-bar,
.lb-overlay.is-chrome-dragging .lb-top { transition: none; }
.lb-overlay.is-leaving .lb-bar,
.lb-overlay.is-leaving .lb-top { transform: translateY(-110%); }
@keyframes lb-bar-in { from { transform: translateY(-110%); } }
.lb-bar { border-bottom: 1px solid var(--bar-edge); }
.lb-bar-inner {
  width: min(1180px, calc(100% - 32px));
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(40px, 1fr) minmax(0, auto) minmax(40px, 1fr);
  align-items: center;
  gap: 14px;
}
.lb-counter {
  grid-column: 1;
  color: var(--bar-muted);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
/* the file name sits in the bar, centred on the bar itself: the two
   side columns match so a hidden counter does not pull it left */
.lb-caption {
  grid-column: 2;
  min-width: 0;
  text-align: center;
  color: var(--bar-muted);
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lb-close {
  grid-column: 3;
  justify-self: end;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--bar-edge);
  border-radius: 2px;
  background: transparent;
  color: var(--bar-ink);
  cursor: pointer;
  display: grid;
  place-items: center;
}
.lb-close:hover { border-color: var(--bar-ink); }
.lb-close svg { display: block; }
.lb-nav {
  position: fixed;
  top: 50%;
  z-index: 4;
  transform: translateY(-50%);
  width: 40px;
  height: 64px;
  border: 0;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  color: #ffffff;
  font: inherit;
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: opacity var(--duration-fast) var(--ease-smooth-out);
}
.lb-prev { left: 10px; }
.lb-next { right: 10px; }

/* The message under a photo: the quest sheet's twin. A paper sheet
   with the same rounded top, the same apron reaching the bottom of
   the screen, the same folding grab bar; it peeks 148px at the
   bottom and drags or flings up to half the screen. The script sets
   its transform; the CSS supplies the springy settle. */
.lb-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  height: 50vh;
  box-sizing: border-box;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-bottom: 0;
  border-radius: 6px 6px 0 0;
  box-shadow: var(--sheet-apron);
  padding: 18px 16px 22px;
  padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
  touch-action: none;
  will-change: transform;
  /* a thrown sheet overshoots a touch and settles back */
  transition: transform 300ms cubic-bezier(0.22, 1.25, 0.36, 1);
}
.lb-sheet.is-dragging { transition: none; }
.lb-sheet.is-going { transition: transform 240ms cubic-bezier(0.3, 0, 0.7, 0.4); }
.lb-overlay.is-leaving .lb-sheet { transition: transform 240ms cubic-bezier(0.3, 0, 0.7, 0.4); }
/* it arrives from below the screen; the script has it at its peek */
.lb-sheet.is-arriving { animation: lb-sheet-in 300ms cubic-bezier(0.2, 0.7, 0.3, 1); }
@keyframes lb-sheet-in { from { transform: translateY(110%); } }
/* the grab bar: two halves hinged in the middle, folding into a v on
   the way down and an up arrow on the way up, stretching in step with
   the rubber band (the script drives --stretch) */
.lb-sheet-handle {
  --stretch: 1;
  flex: none;
  position: relative;
  height: 27px;
  margin: -18px -16px 0;
  cursor: grab;
}
.lb-sheet-handle::before,
.lb-sheet-handle::after {
  content: '';
  position: absolute;
  top: 6px;
  width: 23px;
  height: 5px;
  background: var(--border-strong);
  border-radius: 2px;
  transition: transform 160ms ease;
}
.lb-sheet-handle::before { left: calc(50% - 22px); }
.lb-sheet-handle::after { right: calc(50% - 22px); }
.lb-sheet-handle.is-vee::before { transform: translateY(3px) rotate(15deg); }
.lb-sheet-handle.is-vee::after { transform: translateY(3px) rotate(-15deg); }
.lb-sheet-handle.is-up::before { transform: translateY(3px) rotate(-15deg); }
.lb-sheet-handle.is-up::after { transform: translateY(3px) rotate(15deg); }
.lb-sheet-handle:not(.is-vee):not(.is-up)::before,
.lb-sheet-handle:not(.is-vee):not(.is-up)::after { transform: scaleX(var(--stretch)); }
.lb-sheet-handle.is-stretching::before,
.lb-sheet-handle.is-stretching::after { transition: none; }
.lb-sheet-title {
  flex: none;
  margin: 0;
  padding: 0 0 8px;
  font-weight: 850;
  letter-spacing: -0.02em;
  line-height: 1.25;
}
.lb-sheet-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
}
/* The message beside the photo instead of under it. A sheet across
   the bottom needs vertical room; a computer has plenty but the photo
   is wider than it is tall, and a phone turned sideways has none at
   all. Either way the message goes down the side, at a fixed width,
   and stays there — no peek, no grab bar, nothing to drag. */
.lb-overlay.is-side-panel { --lb-panel-w: min(360px, 34vw); }
.lb-overlay.is-side-panel .lb-sheet {
  top: var(--lb-bar-h);
  bottom: 0;
  left: auto;
  right: 0;
  width: var(--lb-panel-w);
  max-width: none;
  height: auto;
  margin: 0;
  border: 0;
  border-left: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  padding: 18px 18px calc(22px + env(safe-area-inset-bottom, 0px));
  padding-right: calc(18px + env(safe-area-inset-right, 0px));
  touch-action: auto;
  transform: none;
  transition: none;
  animation: none;
}
.lb-overlay.is-side-panel .lb-sheet-handle { display: none; }
.lb-overlay.is-side-panel .lb-sheet-body { overscroll-behavior: auto; }
/* the photo takes the rest of the screen, full height */
.lb-overlay.is-side-panel.has-sheet .lb-frame {
  right: var(--lb-panel-w);
  bottom: 0;
  padding-bottom: var(--lb-bar-h);
}
/* and the next arrow stays off the panel */
.lb-overlay.is-side-panel.has-sheet .lb-next { right: calc(var(--lb-panel-w) + 10px); }
/* the one-tap black screen still takes the whole screen: the panel
   leaves with everything else (a finger can still reach it in
   landscape; a mouse never gets there) */
.lb-overlay.is-side-panel.is-immersive .lb-sheet { opacity: 0; }
.lb-overlay.is-side-panel.is-immersive.has-sheet .lb-frame { right: 0; padding: 0; }
/* too narrow for a side panel even with a mouse: back across the
   bottom, but still a fixed strip rather than a sheet to drag */
@media (max-width: 760px) {
  .lb-overlay.is-side-panel { --lb-panel-w: 0px; }
  .lb-overlay.is-side-panel .lb-sheet {
    top: auto;
    left: 0;
    right: 0;
    width: 100%;
    max-width: 640px;
    height: 40vh;
    margin: 0 auto;
    border-left: 0;
    border-top: 1px solid var(--border);
  }
  .lb-overlay.is-side-panel.has-sheet .lb-frame { bottom: 40vh; padding-bottom: 12px; }
}

/* A mouse: the arrows answer a hover, and a photo that is not zoomed
   is not a thing to drag — dragging pans it once it is. */
.lb-nav:hover { background: rgba(255, 255, 255, 0.24); }
.lb-nav:focus-visible,
.lb-close:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }
.lb-overlay.is-mouse img.lb-media.is-zoomed:active { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
  .lb-overlay, .lb-media, .lb-bar, .lb-top, .lb-sheet { animation: none; }
  .lb-frame, .lb-bar, .lb-top, .lb-sheet, .lb-overlay { transition: none; }
}
/* the apron the sheet paints below itself (see the quest list's
   sheet): the box again every 100px down to 900px */
:root {
  --sheet-apron:
    0 100px 0 var(--surface), 0 200px 0 var(--surface), 0 300px 0 var(--surface),
    0 400px 0 var(--surface), 0 500px 0 var(--surface), 0 600px 0 var(--surface),
    0 700px 0 var(--surface), 0 800px 0 var(--surface), 0 900px 0 var(--surface);
}

/* a button off doing its work: dimmed, with a ticking ellipsis */
button.is-busy { opacity: 0.65; pointer-events: none; }
button.is-busy::after {
  content: '…';
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  vertical-align: bottom;
  width: 0;
  animation: busy-dots 900ms steps(3, end) infinite;
}
@keyframes busy-dots { to { width: 1.05em; } }
@media (prefers-reduced-motion: reduce) {
  button.is-busy::after { animation: none; width: auto; }
}

/* The settings menu's Phone group is only meaningful where the phone
   things are on offer, and the whole group goes with its rows so no
   label is left standing over nothing. App layout: a handheld, in a
   browser tab, at phone width — the installed app is always the app,
   a window this wide has no app layout to choose, and a desktop
   pointing device means a small window rather than a phone, which is
   the same test site.js makes. Haptic feedback: any handheld, since
   the engine runs in the app and in phone browsers alike; a desktop
   pointing device hides it too, whatever the window's width, because
   a mouse does not buzz. Each row keeps a rule of its own as well as
   the group's, so a script asking whether a row is shown gets the
   right answer either way. */
html.is-installed .settings-app-row { display: none; }
@media (min-width: 841px) {
  .settings-app-row { display: none; }
  html:not(.is-pwa) .settings-haptics-row,
  html:not(.is-pwa) .settings-phone-group { display: none; }
}
@media (hover: hover) and (pointer: fine) {
  .settings-app-row,
  .settings-haptics-row,
  .settings-phone-group { display: none; }
}

/* ---------------- Hour bars ----------------
   A bar chart of counts per hour (the recap and the team pages):
   plain boxes, the accent as the bar and a darker inner bar for the
   part that was accepted, uppercase ticks under every few hours. */
.hour-bars-wrap { margin-top: 12px; }
.hour-bars {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 160px;
  padding: 0 0 22px;
  border-bottom: 2px solid var(--border);
  box-sizing: content-box;
}
.hour-col {
  flex: 1;
  min-width: 0;
  height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
.hour-bar {
  width: 100%;
  background: var(--accent);
  border-radius: 2px 2px 0 0;
  position: relative;
  min-height: 0;
}
.hour-bar-inner {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--sign-bg);
  opacity: 0.55;
}
.hour-tick {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 4px;
  text-align: center;
  font-size: 0.62rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
}
.hour-bars-none { color: var(--muted); }

/* ---------------- The index table ----------------
   A wayfinding system's own object for "here is everything and where
   it goes": the route bullet, the destination, what is there. One rule
   at the top, one between rows, nothing else. It was four boxes with
   coloured top edges, which is a card grid wearing the palette. */
.index-table {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  border-top: 1px solid var(--border);
  row-gap: 0;
}

/* The row is display: contents so the link itself is the grid item and
   can take the page's own columns with subgrid. With the <li> in the
   way the subgrid had nothing to inherit from and the table quietly
   built twelve columns of its own, which put it off the page grid. */
.index-row { display: contents; }

.index-row a {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: baseline;
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
  transition: background var(--duration-quick) var(--ease-smooth-out);
}

.index-row a:hover { background: rgba(0, 0, 0, 0.045); }
html[data-theme="dark"] .index-row a:hover { background: rgba(255, 255, 255, 0.06); }

/* No disc. The plate at the top of this page spends blue, orange and
   red on the three kinds of quest; this list was spending the same
   three, plus two more, on five pages, so red meant "bonus quests" at
   the top of the page and "welfare and safety" fifteen hundred pixels
   down it. A route colour naming two different things is the one rule
   this system does not bend on. */
.index-name {
  grid-column: 1 / 5;
  font-size: var(--t-lede);
  font-weight: 800;
  letter-spacing: -0.025em;
}

.index-what {
  grid-column: 5 / 12;
  color: var(--muted);
  font-size: var(--t-body);
  line-height: 1.5;
}

/* the arrow is drawn to the weight of the text it follows */
.index-row a::after {
  content: '';
  grid-column: 12;
  justify-self: end;
  align-self: center;
  width: 0.6rem;
  height: 0.6rem;
  border: solid var(--text);
  border-width: 0 2.5px 2.5px 0;
  transform: rotate(-45deg);
  transition: transform var(--duration-quick) var(--ease-smooth-out);
}

.index-row a:hover::after { transform: translateX(4px) rotate(-45deg); }

@media (max-width: 760px) {
  .index-row a { grid-template-columns: 1fr auto; row-gap: 5px; }
  .index-name { grid-column: 1; }
  .index-what { grid-column: 1; }
  .index-row a::after { grid-column: 2; grid-row: 1; }
}

/* the underline in the footer is doing nothing the position does not */
.site-footer-links a { text-decoration: none; }
.site-footer-links a:hover { text-decoration: underline; }

/* ---------------- The board's clock ----------------
   The most time-sensitive fact on the page, set in the board's own
   type as the last cell of the departure strip. It used to be a
   split-flap module in a narrow rail below the fold: a fourth
   reference borrowed from airport departure boards, in a page built on
   subway signage, and the only decorative gesture on it. Minutes are
   shown because the last hour matters; seconds at thirty days out were
   false precision and a digit that never moves reads as broken.
   The last cell of the strip takes the same label and the same
   baseline as the four beside it, so the one fact that is moving reads
   as part of the row rather than as a widget bolted under it. */
.board-clock { color: var(--sign-ink); }

/* The door, beside the deadline. The plate counted down to sign-ups
   closing and the only way to act on it was a hundred and fifty pixels
   below the fold, at the same weight as two text links. */
.board-go { grid-column: 1 / -1; margin-top: 26px; }

.board-cta {
  display: inline-block;
  background: var(--accent-solid);
  color: var(--on-accent);
  border-radius: 2px;
  padding: 19px 38px;
  font-size: var(--t-lede);
  font-weight: 800;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: background var(--duration-quick) var(--ease-smooth-out);
}

.board-cta:hover { background: var(--accent-dark); }

.board-clock-figs {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 18px;
  margin: 5px 0 0;
}

.board-clock-fig {
  color: var(--sign-muted);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.board-clock-fig b {
  font-size: var(--t-lede);
  font-weight: 800;
  letter-spacing: -0.025em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--sign-ink);
  margin-right: 4px;
}

.board-clock-msg {
  margin: 5px 0 0;
  font-size: var(--t-lede);
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* The black is the page edge, which is what makes it signage rather
   than a content box sitting on paper. A wayfinding system has one
   reading edge, and everything on the plate keeps it. */

/* On the dark ground the plates all but disappear: the sign is #131313
   and the page is #121212, so the alternation the page's whole rhythm
   is built on — paper, signage, paper — reads as one unbroken dark
   field. The sign cannot be lightened (it is the same black in both
   themes, by design) and the page cannot go darker than it already is,
   so the edge is what says where a plate starts and stops. */
html[data-theme="dark"] .board,
html[data-theme="dark"] .plate {
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

/* The SCS mark stands in for the old green SCS.2 badge: the badge keeps
   its class so every rule that hides it in the app bar still applies. */
.site-brand-badge:has(img) { background: none; padding: 0; display: inline-flex; }
.site-brand-badge img { display: block; height: 32px; width: auto; }
.app-splash img { display: block; width: min(38vw, 155px); height: auto; }
.site-footer-brand img { float: left; margin: 2px 14px 4px 0; height: 32px; width: auto; }

/* Submit in the bottom bar: the same rules as site.css, so the five
   pages still on this sheet show the bars the way every other page
   does. */
.welfare-toggle { display: none; }
@media (max-width: 840px) {
  html.is-pwa:not(.submit-float) .tab-bar .tab-welfare,
  html.is-pwa.submit-float .tab-bar .tab-submit,
  html.is-pwa:not(.submit-float) .fab-wrap { display: none; }
  html.is-pwa:not(.submit-float) .welfare-toggle {
    display: inline-grid;
    place-items: center;
    position: absolute;
    left: 40px;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    border: 1px solid transparent;
    border-radius: 2px;
    color: var(--bar-ink);
    text-decoration: none;
  }
  html.is-pwa .welfare-toggle svg { grid-area: 1 / 1; width: 19px; height: 19px; }
  html.is-pwa .welfare-toggle .icon-heart-fill,
  html.is-pwa .welfare-toggle.is-active .icon-heart-outline { display: none; }
  html.is-pwa .welfare-toggle.is-active .icon-heart-fill { display: block; }
  html.is-pwa:not(.submit-float) .site-brand.has-page-title { max-width: calc(100% - 188px); }
}
