/* ============================================================
   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.
     It was briefly the darker #00752f, because white on #00933c is
     4.01:1 and the rest of the site's type is held to 4.5. It came
     back: the dark green read as a different, duller colour rather
     than as the site's green, and one green everywhere is worth more
     here than the ratio. Deliberate, not an oversight. The name stays
     so the two uses of green are still named apart. */
  --accent-solid: #00933c;
  --accent-soft: #e8f2e8;
  --accent-soft-border: #b5d6bc;
  --on-accent: #ffffff;
  --notice-bg: #fdf6e0;
  --notice-border: #e5cd8a;
  --danger-bg: #fdf0ef;
  --danger-border: #eeb4ae;
  /* The ground a destructive button is filled with, and the ink on it.
     Deliberately not --alert-ink: the line red carries white at 4.05:1,
     under the 4.5 the rest of the site's type is held to. This one is
     5.21:1 on white, and reads as a block against the paper (4.53) and
     against the dark ground (3.60). Same value in both themes — see
     .btn-danger. */
  --danger-solid: #d4211c;
  --on-danger: #ffffff;
  /* 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;
  /* The same red as words, but on a sign bar. A sign is the same black
     in both themes, so this cannot follow --alert-ink: the paper red
     buzzes on the black exactly as it does in dark mode. It is the
     dark theme's coral, in both themes, the way --sign-bg and
     --sign-ink are the same in both. */
  --alert-sign: #ff6b60;
  /* 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;
  /* the same green as the light theme, against this theme's own ink */
  --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, bell, theme) */
  --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.
   Neither page-wide slot is free: `html::after` is the app layout's
   cover over the notch, and `body::after` is the quest page's sheet
   dim — 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),
    box-shadow var(--duration-quick) var(--ease-smooth-out);
}

/* 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 — and the bar's own copy of the gear had to turn it off again.
   Outside the bar — the settings menu's own copy on a page — the
   border is still the whole of it. */
@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);
}
/* In the bar the gear hovers the way every other thing in the bar
   hovers: the 3px rule under it that the nav links draw, rather than a
   box round it. The links took that rule the day the outline came off
   at rest, and two hover treatments in one row of controls read as two
   kinds of control. The bar's own ink, because the gear and the bell
   are chrome rather than destinations and have no route colour — the
   links read --route, and there is none to read here. */
@media (hover: hover) and (pointer: fine) {
  .site-nav .settings-toggle:hover {
    border-color: transparent;
    box-shadow: inset 0 -3px 0 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),
    box-shadow var(--duration-quick) var(--ease-smooth-out);
}

/* Welfare's way in from the top bar, used only by the app layout while
   Submit has Welfare's place in the bottom bar (the rules sit with the
   bottom bar's). Everywhere else the page is in the menu. */
.welfare-toggle { display: none; }

/* the same rule under it the nav links and the gear draw */
@media (hover: hover) and (pointer: fine) {
  .bell-toggle:hover {
    border-color: transparent;
    box-shadow: inset 0 -3px 0 var(--bar-ink);
  }
}

/* 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; }
}

/* Inside the button, tucked against the bell's own top right. It used
   to hang outside the corner at -6px, with a ring of the bar's colour
   so it read as clear of the button's outline — and that outline is
   gone. Hanging off a button with no box, under a bar whose hover is a
   rule along the bottom, it read as a mark floating in the bar rather
   than as something on the bell. It still carries the ring, because it
   overlaps the glyph now rather than the button's edge, and a red disc
   straight on a black bell is one shape with a bite out of it. */
.bell-dot {
  position: absolute;
  top: 7px;
  right: 7px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--line-red);
  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;
  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 a panel 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 panel as usual. The quest
   list's filter drawer uses the same one — it is the same kind of thing
   over the same page, and two panels with two different treatments read
   as two different systems. Neither carries a shadow: the veil is what
   says the panel is off the page, and a shadow under something already
   lifted by a dimmed screen is a second answer to a settled question. */
html.settings-open body::before,
html.settings-closing body::before,
html.ql-filters-open 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; }
}

/* The panel's two pages. With everything in it the menu was nine rows
   long, and a list that has to be scrolled to reach its end is a list
   nobody reads the end of. What stays on the first page is what a
   reader opens the menu for; the rest is behind the Visual row. One
   page leaves to one side while the next arrives from the other, and
   the panel's own height is tweened between the two from script (they
   are different lengths, and a box that jumps is the one moment the
   reader has to look again to work out what changed). --page-dir is
   1 going in and -1 coming back. */
.settings-panel { overflow-x: hidden; }
.settings-page[hidden] { display: none; }

/* the leaving page comes out of the flow so both occupy the same box
   while they cross; the offsets are the panel's own padding */
.settings-page.is-leaving {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  pointer-events: none;
  animation: settings-page-out var(--duration-fast) var(--ease-smooth-out) forwards;
}
.settings-page.is-arriving {
  animation: settings-page-in var(--duration-fast) var(--ease-smooth-out);
}
@keyframes settings-page-in {
  from {
    opacity: 0;
    transform: translateX(calc(var(--page-dir, 1) * var(--distance-base)));
    filter: blur(var(--blur-medium));
  }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes settings-page-out {
  from { opacity: 1; transform: none; filter: none; }
  to {
    opacity: 0;
    transform: translateX(calc(var(--page-dir, 1) * var(--distance-base) * -1));
    filter: blur(var(--blur-medium));
  }
}
@media (prefers-reduced-motion: reduce) {
  .settings-page.is-arriving,
  .settings-page.is-leaving { animation: none; }
}

/* The row that opens the second page, and the way back from it. The
   chevron is boxed to the rocker's width so the right-hand column
   stays one column. */
.settings-more-btn {
  display: inline-grid;
  place-items: center;
  width: calc(5.2em + 2px);
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  background: var(--surface);
  color: var(--text);
  font-size: var(--t-micro);
  cursor: pointer;
  transition: background-color var(--duration-quick) var(--ease-smooth-out);
}
.settings-more-btn svg { width: 15px; height: 15px; }
@media (hover: hover) and (pointer: fine) {
  .settings-more-btn:hover { background: var(--page-bg); }
}
.settings-more-btn:active { background: var(--border-strong); }
.settings-more-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

.settings-back {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  padding: 4px 6px 4px 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
  transition: color var(--duration-quick) var(--ease-smooth-out);
}
.settings-back svg { width: 14px; height: 14px; flex: none; }
@media (hover: hover) and (pointer: fine) {
  .settings-back:hover { color: var(--text); }
}
.settings-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.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,
.settings-row > .settings-more-btn {
  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,
.sub-rocker: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 .sub-rocker,
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 .sub-rocker > 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-rocker > 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.

   Unless full screen is where the quest list STARTS (html.ql-map-home,
   the settings menu's Full screen map). Then it is the page rather than
   a layer over it, and a page with no way off it is a trap: the app
   layout keeps its bottom bar, which is how you leave any page there,
   and everything else keeps the top bar. The footer and the floating
   button go either way — the footer is the end of a scroll there is
   none of, and the bar already carries Submit a Quest. */
html.ql-map-full .site-footer,
html.ql-map-full .fab-wrap { display: none; }
html.ql-map-full:not(.ql-map-home) .site-nav,
html.ql-map-full:not(.ql-map-home) .tab-bar,
html.ql-map-full.ql-map-home.is-pwa .site-nav { 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. */
/* The page's name on a black band that runs the full width of the
   screen — the same object `.plate` is, which is what the quest list,
   the messages page, the gallery and submit already put their names
   on. It used to be a black box sitting on the paper with a shadow
   under it, which reads as a card rather than as the signage a page
   name belongs on, and it meant the site had two answers to the same
   question depending on which page you opened.

   It bleeds by the shell's own half-gutter, so it needs no viewport
   arithmetic and brings no sideways scrollbar, and it pulls up by the
   shell's top padding so the black meets the nav's route stripe with
   no strip of paper between them. */
/* One gap between the page's name and what comes under it, on every
   page: 32px on a wide screen, 24px on a phone. In the app layout the
   name is in the bar and the band is not drawn, so the same 24px is the
   gap from the bar to the first thing on the paper. Whatever follows the
   band gives up its own top margin, or the gap is the band's plus that
   element's and differs from page to page, which is what it did — 28, 29,
   40 and 48 on five pages. */
:root { --after-title: 32px; }
main > .hero + * { margin-top: 0; }
/* A section heading pulls itself up into the margin above it so its
   rule sits where the gap starts. First under the band there is no
   margin left to pull into, and it rode up over the band's edge. */
main > .hero + * > .section-heading:first-child { margin-top: 0; }
html.is-pwa .page-shell { --shell-top: var(--after-title); }

.hero {
  --bleed-m: calc(50% - 50vw);
  --bleed-p: calc(50vw - 50%);
  display: block;
  border: none;
  margin-inline: var(--bleed-m);
  margin-top: calc(var(--shell-top, 32px) * -1);
  margin-bottom: var(--after-title);
  padding: 30px var(--bleed-p);
  background: var(--sign-bg);
  color: var(--sign-ink);
  text-align: left;
}

/* the band is the bar, so the bar keeps no box of its own */
.hero .hero-board { gap: 10px; }
.hero h1.sign-bar,
.hero .hero-copy.sign-bar {
  background: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0;
}

/* ---------------- 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;
}

/* The 22px here was the inset of a black box sitting on the paper.
   There is no box any more — the band is the bar — so it would only
   push the title 22px right of the date, the run sheet and everything
   else standing on the page's own gutter. Same specificity as the
   reset above and written after it, which is how it kept winning. */
.hero h1.sign-bar {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  color: var(--sign-ink);
  padding: 0;
}

/* 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. */
/* The rule sits in the middle of the gap it opens. It used to have
   about forty pixels of paper above it and twelve below, so it read as
   stuck to the heading under it rather than dividing one block of
   reading from the next — which is what it is, on the pages that are
   text separating text. The negative top margin is what moves it: the
   padding grows and the margin takes the same amount back, so the
   distance from the last paragraph to the heading is unchanged and
   only the rule inside that gap moves down. Two values, because the
   gap itself is wider on a desktop than on a phone and one number
   cannot sit in the middle of both. */
.section-heading {
  margin: -14px 0 16px;
  padding-top: 26px;
  border-top: 6px solid var(--text);
  font-size: var(--t-head);
}

@media (min-width: 841px) {
  .section-heading { margin: -22px 0 18px; padding-top: 34px; }
}

/* Not on the submit page. A heading there sits over a form rather than
   between two blocks of reading, and a divider floating above a set of
   controls belongs to neither — including its rule, which under the
   Submit/Compress row was a second line drawn across a boundary the
   row's own hairline had already drawn. */
.sub-page .section-heading {
  margin: 0 0 4px;
  padding-top: 12px;
  border-top: 0;
}

/* ---------------- 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 rule's own negative margin (above) already carries this, so the
   old -6px nudge would count twice. Kept on the submit page, which
   keeps the old spacing. */
.sub-page .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);
}

/* The button that does the damage. Red here is words: the paper stays
   the paper and the border takes its colour from the label, rather
   than a red field, which would read as one of the routes. */
/* The one button that undoes something. It was red words on the page's
   own surface, which reads as a link that happens to be red and sat at
   the same weight as Cancel beside it. Filled, it is the only solid
   block on the panel and the eye goes to it, which is what a last step
   is for. One red in both themes, for the same reason the signage bar
   is one black in both: it is a solid ground with reversed type on it,
   not ink on paper, so it does not follow the page. */
.btn-danger {
  background: var(--danger-solid);
  border-color: var(--danger-solid);
  color: var(--on-danger);
}

/* 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); }
.prose a.btn-danger { color: var(--alert-ink); }

/* ---------------- 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;
}

/* ---------------- Justified text ---------------- */

/* Off by default; the Visual settings page turns it on and the class
   goes on <html>. A page opts its running text in with data-justify on
   the block that holds it, rather than the rule reaching for every
   paragraph on the site — a label, a caption or a line in a table cell
   is not running text, and justifying a narrow cell opens rivers of
   white space through it.

   Hyphenation is not optional here. Justify stretches the spaces
   between words to reach both margins, and with no way to break a long
   word the stretch lands in one or two gaps; hyphens: auto is what
   spreads it. hyphenate-limit-chars keeps it off short words and off
   two-letter fragments. Safari needs the prefix. text-wrap: pretty
   stops the last line of a paragraph being one word on its own, which
   justify makes more obvious rather than less. */
html.justify-text [data-justify] p,
html.justify-text [data-justify] li,
html.justify-text p[data-justify] {
  text-align: justify;
  text-wrap: pretty;
  -webkit-hyphens: auto;
  hyphens: auto;
  hyphenate-limit-chars: 7 3 3;
}
/* A row of links is a list too, and its items are one line each. */
html.justify-text [data-justify] .index-rows li,
html.justify-text [data-justify] .chip-row li { text-align: left; }

/* ---------------- 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);
}

/* The control, and the words beside it. Where the words carry a link
   the label goes round the control alone (.sub-rocker) — the quest
   page's rockers learned the same thing for a different reason, that a
   label spanning the row flips when a thumb lands on the words. */
.sub-rocker {
  display: inline-flex;
  align-items: center;
  flex: none;
  cursor: pointer;
}

/* 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)); }
/* unless the top bar is still there, which it is when full screen is
   where the quest list starts */
html.ql-map-full.ql-map-home:not(.is-pwa) .toast-stack { top: calc(var(--nav-h, 65px) + 10px); }

.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. The app layout puts the same
   link in the middle of its bottom bar instead, unless the Visual
   page's Submit in the bottom bar is off (the bottom bar's rules). */
/* 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 to
   the same 0.4 it takes when the top bar has gone, on the menu's own
   clock, and stops taking taps, so a tap where it is is a tap outside
   the menu, which closes it. It used to go to 0 and read as the button
   having vanished — it is above the veil, so it cannot be left alone
   either: it would be the one bright thing on a darkened page. */
html.settings-open .fab-wrap {
  opacity: 0.4;
  pointer-events: none;
  transition: opacity var(--dropdown-open-dur) var(--ease-smooth-out);
}
html.nav-up .fab-wrap { opacity: 0.4; }
/* Coming back it rides the veil's clock rather than the chrome's: it
   used to hold its dim until the menu had gone and then take the
   chrome's half-second back, so it arrived well after the page it sits
   on had brightened. Still no taps until the menu is fully gone. */
html.settings-closing .fab-wrap {
  pointer-events: none;
  transition: opacity var(--dropdown-close-dur) var(--ease-smooth-out);
}
/* 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.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, and the quest page's
   search bar, which lines itself up with the button, went with 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)); }
/* the same gap it keeps from the right edge, measured off the bar
   rather than guessed at — site.js writes --tabbar-h */
html.is-pwa .fab-wrap { bottom: calc(var(--tabbar-h, 57px) + 19px); }

/* ---------------- 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; what is being
   typed into drops into the space it had. --kb-inset (site.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 is set whenever a text field has focus on a handheld. */
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: 0;
  overflow: hidden;
  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);
}
/* The name on the sign, the reason on the paper: the same two-part
   object the page heroes and the confirm panel are. It used to be a
   bold line of page ink over grey body text inside one white box,
   which is the one shape this design system does not have. The bar is
   flush into the card's own edge, so it keeps neither a corner of its
   own nor a second shadow. */
/* `.install-nudge p` is written after .sign-bar and outranks it, so
   the title came out muted grey on the black — the one colour on the
   card that had to be the sign ink. Named twice here to win it back. */
.install-nudge .install-nudge-head {
  color: var(--sign-ink);
  margin: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 11px 16px;
  font-size: var(--t-small);
  font-weight: 800;
  letter-spacing: -0.01em;
}
.install-nudge-body { padding: 13px 16px 14px; }
.install-nudge p { margin: 0 0 10px; color: var(--muted); font-size: 0.92rem; line-height: 1.5; }
.install-nudge-more { margin: 0 0 12px; }
.install-nudge-more a {
  color: var(--accent-dark);
  font-weight: 800;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-underline-offset: 3px;
}
.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);
  }
  /* Each tab lights in its page's route colour, the one the desktop bar
     gives the same page. Keyed by page rather than by position, so the
     colours stay with their pages when Submit takes the middle. */
  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; }

  /* ---- Submit in the bottom bar ----
     The app layout's default: Submit takes the middle of the bar and
     Welfare moves up beside the bell, so the floating button goes and
     nothing is left sitting over the page. html.submit-float (Submit
     in the bottom bar, off, on the Visual page) is the other way round:
     Welfare's tab and the floating button. */
  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; }

  /* Welfare in the top bar, hard against the bell and drawn the way the
     bell is: bar ink, no box, the solid face on its own page. */
  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; }

  /* 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));
  }
  /* With the bar away the strip has nothing left to stand in for, and
     the body's background is the screen's, not the page's: it stayed
     at the top of the screen as a band of the bar's white over the
     paper while the page scrolled under it. */
  html.is-pwa.nav-up body { background-size: 100% 0; }

  /* 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, and it is the
     PAGE's ground rather than the bar's: with the bar gone there is no
     bar for it to continue, so drawing it in the bar's white put a
     stray white band across the top of a paper page every time the
     chrome slid away. In the page's own colour it is invisible as a
     band and still stops the content reaching the cutout.
     `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 */
    /* site.js reads what is actually under the notch and writes it here
       while the bar is away; the page's own paper is the fallback and
       is right on every page whose top is paper. On the home page it is
       the board's black, and the paper painted across it read as a pale
       band on the black — white in the light theme, grey in the dark. */
    background: var(--cutout-bg, var(--page-bg));
    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;
  }
  /* the title is centred, so the heart beside the bell takes the same
     room off both ends */
  html.is-pwa:not(.submit-float) .site-brand.has-page-title {
    max-width: calc(100% - 188px);
  }
  /* The home page's bar is the mark and the season's name. On a phone
     that no longer fits between the heart and the gear, so the mark
     stands alone: the name is the headline straight under it. */
  @media (max-width: 440px) {
    html.is-pwa:not(.submit-float) .site-brand:not(.has-page-title) .site-brand-name { display: none; }
  }
  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) {
  /* --shell-top has to follow the shell's own padding here, or the
     band pulls up by 32 to meet a bar that moved to 18 and overshoots
     by fourteen. */
  :root { --after-title: 24px; }
  .page-shell { --shell-top: 18px; width: min(100% - 32px, 1180px); padding-top: var(--shell-top); }
  html.is-pwa .page-shell { --shell-top: var(--after-title); }
  .hero { padding: 22px var(--bleed-p); }
  .hero h1.sign-bar { 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); }
/* The arrow parks with its bottom edge at the bottom of whatever is
   pinned to the top of the page: the bar, and on a page that has one a
   sticky row under it — the quest list's chips, the submit page's view
   switch. site.js measures that row into --ptr-lift. Without it the
   arrow sat 44px above the page's content, which in the app layout is
   inside the chip row's own band, so the pull slid it out through the
   middle of the row.
   The row has to hold still for this to read: it is counter-translated
   below, so the page moves down under it the way it moves down under
   the bar, and the arrow comes out from behind it. */
.ptr {
  position: absolute;
  left: 50%;
  top: calc(-44px + var(--ptr-lift, 0px));
  z-index: -1; /* beneath the page's content, whatever happens */
  pointer-events: none;
  transform: translateX(-50%);
}
/* Whatever is stuck to the top of the page stays where it is while the
   page slides down under it — a boundary that travels with the thing it
   bounds is not one, and the arrow has nothing to come out from under.
   The pull only ever runs at the top of the page, where nav-up is off,
   so nothing else is using these transforms at the time. */
html.is-pulling .ptr-still,
html.is-pull-settling .ptr-still { transform: translateY(calc(var(--pull, 0px) * -1)); }
html.is-pull-settling .ptr-still { transition: transform var(--duration-fast) var(--ease-smooth-out); }
.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; }
}

/* ---------------- Slide to confirm ----------------
   In front of something that cannot be undone: a thumb slid the whole
   way across and released at the far end, then a plain button on a
   second panel. One card on dimmed ground, built by
   window.scs.confirmSlide.

   Three sizes and one black object. The question sits on the card's
   own sign bar rather than under an eyebrow repeating it, the copy and
   the buttons read at body size, and the only label — the one in the
   track — is the site's uppercase label voice. The word that does the
   damage is red; nothing else in the card asks to be looked at. */
.confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 1460;
  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);
}

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

.confirm-card {
  width: min(430px, 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);
}

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

/* The two steps are one card. The panel that arrives raises itself out
   of a blur while site.js tweens the card between the two heights, so
   the step reads as one movement rather than a swap. overflow goes hard
   for the length of it: `auto` would put a scrollbar up while the card
   is still shorter than what is already in it. */
@keyframes confirm-step-in {
  from {
    opacity: 0;
    transform: translateY(var(--distance-medium));
    filter: blur(var(--blur-small));
  }
}
.confirm-card.is-stepping { overflow: hidden; }
.confirm-card.is-stepping > * {
  animation: confirm-step-in var(--duration-fast) var(--ease-smooth-out) both;
}
@media (prefers-reduced-motion: reduce) {
  .confirm-card.is-stepping > * { animation: none; }
}

/* Flush into the card's own edge: the bar is the top of the card, so
   it keeps neither a corner of its own nor the shadow the card is
   already casting. */
.confirm-title.sign-bar {
  margin: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 14px 18px;
  font-size: var(--t-head);
  line-height: 1.2;
  letter-spacing: -0.025em;
}

.confirm-body { padding: 18px; }

.confirm-copy {
  margin: 0;
  color: var(--muted);
  font-size: var(--t-body);
  line-height: 1.6;
}

.confirm-slide {
  position: relative;
  height: 52px;
  margin: 16px 0 0;
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  background: var(--page-bg);
  touch-action: none;
  overflow: hidden;
}

/* Centred in the run the thumb has left, not in the whole track: the
   thumb is parked over the first 46px of it. */
.confirm-slide-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 46px;
  color: var(--muted);
  font-size: var(--t-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  pointer-events: none;
}

.confirm-slide-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 46px;
  height: 44px;
  padding: 0;
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 2px;
  color: var(--text);
  cursor: grab;
  touch-action: none;
  /* The colour arrives and leaves on a clock; the transform never does,
     because it tracks the finger. Enumerated rather than shorthand for
     that reason — `all` would put a lag between the thumb and the
     hand. */
  transition:
    background-color var(--duration-quick) var(--ease-in-out),
    border-color     var(--duration-quick) var(--ease-in-out),
    color            var(--duration-quick) var(--ease-in-out);
}

.confirm-slide-thumb svg { width: 20px; height: 20px; }
.confirm-slide-thumb:active { cursor: grabbing; }
/* letting go early eases the thumb home; while the finger holds it the
   transform transition stands down so it tracks exactly. The colour
   clock is restated here because this is the whole shorthand. */
.confirm-slide-thumb.is-settling {
  transition:
    transform        var(--duration-fast)  var(--ease-smooth-out),
    background-color var(--duration-quick) var(--ease-in-out),
    border-color     var(--duration-quick) var(--ease-in-out),
    color            var(--duration-quick) var(--ease-in-out);
}
/* far enough to count: the thumb says so before the finger lets go */
.confirm-slide-thumb.is-armed {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent);
}

.confirm-actions {
  margin-top: 16px;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

@media (prefers-reduced-motion: reduce) {
  .confirm-overlay, .confirm-card,
  .confirm-slide-thumb, .confirm-slide-thumb.is-settling { 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; }
/* Submit in the bottom bar moves things between the app layout's two
   bars, so it shows only where those bars are. */
html:not(.is-pwa) .settings-bar-row { display: none; }
@media (min-width: 841px) {
  .settings-app-row,
  .settings-bar-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; }
