/* celyrian.com
 *
 * Two registers under one piece of chrome. The bar and the hint strip stay put
 * the way a system UI does; the home screen and the map scroll between them.
 *
 * The home screen borrows the Nintendo Switch HOME menu grammar: the focused
 * title above the row, square tiles with a small radius, a focus ring that
 * breathes, a row of round system buttons, and a hint strip wired to the keys
 * it names. The map below borrows Super Mario Bros. Wonder: a beaded route that
 * flows down through every node and milestone, numbered nodes, a pennant on every
 * chapter that is finished, tilted plaques with a thick edge and a printed
 * face, papercraft offsets and a paper grain.
 *
 * The palette is pastel: five accents and tinted neutrals, declared once in
 * the token block and resolved per theme.
 *
 * Nothing here is a Nintendo asset. The grammar is copied, the artwork is not.
 *
 * Rules this file keeps:
 *   - five accents plus neutrals, declared once, no one-off hex below the tokens
 *   - every interactive thing has a rest, a hover, a focus and a press state
 *   - ambient motion is small and slow, and it stops under reduced motion
 */

/* ---------- fonts --------------------------------------------------------- */

@font-face {
  font-family: "Fredoka";
  src: url("/fonts/fredoka-var.6a3a3821.woff2") format("woff2");
  font-weight: 300 700;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku";
  src: url("/fonts/zen-regular.4e6a2358.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku";
  src: url("/fonts/zen-medium.9d2cf4c5.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku";
  src: url("/fonts/zen-bold.aa4182cd.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jbm-regular.90b56724.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* ---------- tokens -------------------------------------------------------- */

/* The palette. Everything between the two markers is colour; a preview build
   swaps this block for another palette file and nothing else changes. */
/* @palette */
:root {
  color-scheme: dark;

  /* warm charcoal: the neutrals carry a little of the accents' warmth */
  --field: #1e1b22;
  --tile: #2a2630;
  --tile-hi: #332e3a;
  --ink: #efe8e6;
  --ink-2: #aa9fa8;
  --rule: #3b3542;

  --paper: #191720;        /* the map sits below the chrome */
  --plaque: #241f2a;
  --plaque-ink: #efe8e6;
  --plaque-ink-2: #bdb2ba;
  --edge: #6e6478;
  --drop: #120f16;         /* the papercraft offset */

  /* five accents: peach to rose, and a mint for the chapter still running */
  --a1: #fcd5b5;
  --a2: #f9c08f;
  --a3: #f5a58c;
  --a4: #f3b3c1;
  --a5: #b8e0c2;
  /* what is drawn on top of an accent, and the ring that marks focus */
  --on-hue: var(--field);
  --focus: var(--a3);

  /* the route: a grey that stands off the paper without shouting, and the
     light on each bead */
  --trail: #5a515f;
  --trail-2: rgb(255 255 255 / 0.22);

  /* material: an edge light, an edge shade, a soft cast shadow, and the sheen
     a tile shows where the pointer is. Alpha overlays, not colours. */
  --hi: rgb(255 255 255 / 0.07);
  --lo: rgb(0 0 0 / 0.32);
  --shade: rgb(0 0 0 / 0.36);
  --sheen: rgb(255 255 255 / 0.09);

  /* the face of a card: a dot grid at a few percent, the way a printed
     panel is */
  --tex-ink: rgb(255 255 255 / 0.055);
  --tex: radial-gradient(var(--tex-ink) 1.1px, transparent 1.6px) 0 0 / 9px 9px;

  /* post-it paper and the ink on it. The same in both themes: a note is a
     thing lying on the plaque, not part of the chrome. */
  --note-a: #fbf0a6;
  --note-b: #fbcad4;
  --note-c: #cfeab8;
  --note-d: #c3e2f8;
  --note-ink: #2a2530;

  /* paper grain for the map: white noise at a few percent, tiled */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.075 0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* Light. The accents deepen a step so a dark glyph and a white card both hold
   against them; the trail and the card face flip to dark-on-light. */
[data-theme="light"] {
  color-scheme: light;
  --field: #f7f3f0;
  --tile: #ffffff;
  --tile-hi: #ffffff;
  --ink: #2a2530;
  --ink-2: #6f6472;
  --rule: #e6ddd9;
  --paper: #fbf8f5;
  --plaque: #ffffff;
  --plaque-ink: #2a2530;
  --plaque-ink-2: #5b515e;
  --edge: #b9adb4;
  --drop: #ddd2ce;
  --a1: #f4b78c;
  --a2: #efa470;
  --a3: #e98d78;
  --a4: #e79bae;
  --a5: #8fcfa6;
  --on-hue: #2a2530;
  --focus: #d06d4c;
  --trail: #c9bfc3;
  --trail-2: rgb(255 255 255 / 0.7);
  --hi: rgb(255 255 255 / 0.9);
  --lo: rgb(0 0 0 / 0.08);
  --shade: rgb(0 0 0 / 0.14);
  --sheen: rgb(0 0 0 / 0.045);
  --tex-ink: rgb(0 0 0 / 0.05);
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
}
/* @/palette */

:root {
  --f-ui: "Zen Kaku", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --f-display: "Fredoka", "Zen Kaku", ui-rounded, system-ui, sans-serif;
  --f-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --r-tile: 10px;
  --r-plaque: 20px;
  --r-chip: 999px;

  /* one overshoot curve for things that pop, one strong ease-out for things
     that travel. Two curves is a house style; five is noise. */
  --pop: cubic-bezier(0.34, 1.56, 0.64, 1);
  --out: cubic-bezier(0.22, 1, 0.36, 1);
  --t-fast: 0.11s;
  --t: 0.2s;

  --bar: 58px;
  --hint: 46px;
  --peek: 76px;
  --wrap-home: 1180px;
  --wrap-map: 780px;
  --wrap-meander: 1180px;
}

/* Each chapter has one accent. The tile and the chapter share it through a
   data attribute, so the row and the map agree without either repeating it. */
[data-hue="a"] { --hue: var(--a1); }
[data-hue="b"] { --hue: var(--a2); }
[data-hue="c"] { --hue: var(--a3); }
[data-hue="d"] { --hue: var(--a4); }
[data-hue="e"] { --hue: var(--a5); }

/* ---------- base ---------------------------------------------------------- */

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--field);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 1rem;
  line-height: 1.7;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}
/* the hint strip is fixed, and it only exists when the script is running */
.js body { padding-bottom: var(--hint); }

h1, h2 {
  font-family: var(--f-display);
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.15;
  margin: 0;
}

p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }
svg { display: block; }

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  padding: 12px 18px;
  background: var(--tile);
  border-radius: 0 0 var(--r-tile) 0;
}
.skip:focus { left: 0; }

/* ===================== the chrome ====================================== */

.bar {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--bar);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 22px;
  background: var(--field);
  border-bottom: 1px solid var(--rule);
}

.me {
  display: flex;
  align-items: center;
  gap: 11px;
  font-weight: 500;
  letter-spacing: 0.005em;
}
/* The avatar is the owner's and stays. It is framed a little tighter than the
   file, which drops the edges and keeps the face. */
.ava {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  display: block;
  box-shadow: 0 0 0 2px var(--field), 0 0 0 3px var(--edge);
}
.ava img {
  display: block;
  width: 100%;
  height: 100%;
  transform: scale(1.22);
  transform-origin: 50% 46%;
}

.status {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 16px;
}

/* the clock: where this page is written it is this time, and the reader's
   own clock is this far from it */
.here {
  justify-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  line-height: 1.2;
  text-align: center;
}
.clock {
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.tz {
  font-size: 0.6875rem;
  color: var(--ink-2);
  white-space: nowrap;
}
.tz-short { display: none; }

/* the pill toggle */
.pill {
  display: none;
  align-items: center;
  gap: 9px;
  border: 0;
  padding: 5px;
  background: none;
  color: var(--ink-2);
  font: inherit;
  font-size: 0.8125rem;
  cursor: pointer;
  border-radius: var(--r-chip);
  transition: color var(--t) var(--out);
}
.js .pill { display: inline-flex; }
.pill:hover { color: var(--ink); }
.pill-track {
  width: 40px;
  height: 23px;
  border-radius: var(--r-chip);
  background: var(--rule);
  padding: 3px;
  display: block;
  box-shadow: inset 0 1px 2px var(--lo);
  transition: background var(--t) var(--out);
}
.pill-knob {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--ink-2);
  display: block;
  box-shadow: 0 1px 2px var(--shade);
  transition: transform var(--t) var(--pop), background var(--t) var(--out);
}
.pill[aria-pressed="true"] .pill-track { background: var(--a3); }
.pill[aria-pressed="true"] .pill-knob {
  transform: translateX(17px);
  background: var(--on-hue);
}
.pill:active .pill-knob { transform: scale(0.9); }
.pill[aria-pressed="true"]:active .pill-knob { transform: translateX(17px) scale(0.9); }

/* The hint strip. Fixed, because the keys it names work anywhere on the page,
   and a legend that scrolls away stops being a legend. Prompts sit on the
   right, the way the Switch draws its button prompts. */
.hints {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  height: var(--hint);
  align-items: center;
  justify-content: flex-end;
  gap: 24px;
  padding: 0 26px;
  background: var(--field);
  border-top: 1px solid var(--rule);
  color: var(--ink-2);
  font-size: 0.8125rem;
}
.js .hints { display: flex; }
.hints span {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
/* the keys are buttons: dark grey caps with a light edge, pressed down on
   click */
.key {
  border: 0;
  padding: 0;
  margin: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  border-radius: 13px;
  -webkit-tap-highlight-color: transparent;
}
.key:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
kbd {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  min-width: 26px;
  height: 26px;
  padding: 0 9px;
  display: inline-grid;
  place-items: center;
  border-radius: 13px;
  background: var(--tile-hi);
  color: var(--ink);
  line-height: 1;
  box-shadow:
    inset 0 1px 0 var(--hi),
    0 0 0 1px var(--rule),
    0 2px 0 var(--drop);
  transition: transform var(--t-fast) var(--out), box-shadow var(--t-fast) var(--out), background var(--t) var(--out);
}
.key:hover kbd { background: var(--rule); }
.key:active kbd { transform: translateY(2px); box-shadow: inset 0 1px 0 var(--hi), 0 0 0 1px var(--rule), 0 0 0 var(--drop); }
.hints span .key + .key { margin-left: 3px; }

/* ===================== register A: the home screen ====================== */

/* Sized so the map shows at the bottom edge. A full-height screen with nothing
   under it gives the reader no reason to believe there is more. */
.home {
  min-height: calc(100svh - var(--bar) - var(--peek));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 30px;
  max-width: var(--wrap-home);
  margin: 0 auto;
  padding: 40px 22px 34px;
}

/* ---------- the title above the row --------------------------------------- */

/* Title and row travel together. On a phone the block is as wide as the
   screen and the row scrolls inside it; from 900px up it is exactly as wide
   as the row and sits in the middle, so the system buttons under it share
   its axis and the title stays on the first tile's left edge. */
.deck {
  display: flex;
  flex-direction: column;
  gap: 30px;
  min-width: 0;
}

/* Height is reserved so the row does not jump when the text swaps: the
   script measures every chapter's words and sets the tallest. */
.plate {
  display: none;
  min-height: var(--plate-h, 96px);
  max-width: 62ch;
  padding-left: 2px;
  --dir: 1;
}
.js .plate { display: block; }

.row-kicker {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: 2px;
}
.row-title {
  font-family: var(--f-display);
  font-weight: 500;
  font-size: 1.55rem;
  line-height: 1.25;
  color: var(--ink);
}
.row-teaser {
  margin-top: 3px;
  color: var(--ink-2);
  font-size: 0.9375rem;
  line-height: 1.6;
}
/* The swap is driven by focus, so it is a reader-initiated move. The old text
   leaves against the direction of travel and the new text arrives from it,
   so moving right reads as moving right. */
.plate {
  transition: opacity 0.09s var(--out), transform 0.09s var(--out);
}
.plate.out {
  opacity: 0;
  transform: translateX(calc(var(--dir) * -8px));
}
.plate.in { animation: plate-in 0.32s var(--pop) both; }
.plate.in .row-teaser { animation: plate-in 0.32s var(--pop) 0.05s both; }
.plate.dir-l { --dir: -1; }
@keyframes plate-in {
  from { opacity: 0; transform: translateX(calc(var(--dir) * 12px)); }
  to   { opacity: 1; transform: none; }
}

/* ---------- the tile row -------------------------------------------------- */

.row {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  /* A flex item defaults to min-width:auto and refuses to shrink below its
     content. It is stretched to the cross axis here so it does not bite today,
     but the failure is a page that scrolls sideways on a phone. */
  min-width: 0;
  margin-inline: -22px;
  padding-inline: 22px;
  scroll-snap-type: x proximity;
  scroll-padding-inline: 22px;
}
.row::-webkit-scrollbar { display: none; }
.row li { scroll-snap-align: start; }
.row ul {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 122px;
  gap: 15px;
  align-items: start;
  /* The ring, its glow, the lift and the tilt are all painted outside the
     tile box, and on a phone this row is a scroller that clips at its edge,
     so the list carries slack on every side and takes it back with margins. */
  padding: 26px 12px 28px;
  margin: -12px;
}

.row a {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 9px;
  text-decoration: none;
  color: var(--ink-2);
  font-size: 0.8125rem;
  -webkit-tap-highlight-color: transparent;
}

/* The tile. Its transform is composed from custom properties so that the
   tilt, the lift, the scale and the press can each set one value without
   restating the others. The script drives the tilt and the sheen from the
   pointer; everything else is CSS state. */
.row a b {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--r-tile);
  overflow: hidden;
  display: grid;
  place-items: center;
  --rx: 0deg;
  --ry: 0deg;
  --s: 1;
  --ty: 0px;
  --mx: 50%;
  --my: 40%;
  /* a solid colour, the way the Switch draws a software icon, with the
     sheen the pointer moves and a little edge light */
  background:
    radial-gradient(circle at var(--mx) var(--my), var(--sheen) 0, transparent 62%),
    var(--hue);
  box-shadow:
    inset 0 1px 0 var(--hi),
    inset 0 -2px 0 var(--lo),
    0 3px 8px var(--shade);
  transform:
    perspective(720px)
    rotateX(var(--rx))
    rotateY(var(--ry))
    translateY(var(--ty))
    scale(var(--s));
  transition:
    transform var(--t) var(--pop),
    box-shadow var(--t) var(--out);
}
/* while the pointer is on it the tile follows, so the spring gets out of the way */
.row a.tilt b { transition-duration: 0.09s; transition-timing-function: var(--out); }

/* the chapter number, small, in the corner, cut out of the colour */
.row a b .n {
  position: absolute;
  top: 8px;
  left: 11px;
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--on-hue);
  letter-spacing: -0.01em;
}

/* The glyph: one filled, rounded silhouette per tile, cut out of the colour.
   Material Symbols Rounded, placed by tools/icons.py. */
.row a b .art {
  width: 56%;
  height: 56%;
  fill: var(--on-hue);
  /* some glyphs are drawn past their own viewBox at this weight (hub does),
     and a viewport clips by default */
  overflow: visible;
  transition: transform var(--t) var(--pop);
}

/* the specular sweep: a band of light crosses the tile once when it becomes
   the selected one. It is the "you picked this" flash. */
.row a b::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(105deg, transparent 32%, var(--hi) 46%, rgb(255 255 255 / 0.18) 50%, var(--hi) 54%, transparent 68%);
  transform: translateX(-110%);
}
.row a.is-on b::before { animation: sweep 0.6s var(--out) 1; }
@keyframes sweep {
  0%   { opacity: 1; transform: translateX(-110%); }
  100% { opacity: 1; transform: translateX(110%); }
}

/* states */
.row a:hover b { --ty: -3px; }
.row a:hover b .art { transform: scale(1.06); }

/* Selection. The script keeps this on whichever tile the title is describing,
   so the ring and the title can never disagree. Without the script nothing is
   selected, which is honest, because nothing is tracking a cursor. The ring
   breathes the way the Switch cursor does. */
.row a.is-on b,
.row a:focus-visible b {
  --s: 1.08;
  animation: ring 1.7s ease-in-out infinite;
  box-shadow:
    inset 0 1px 0 var(--hi),
    inset 0 -1px 0 var(--lo),
    0 0 0 3px var(--field),
    0 0 0 6px var(--ink),
    0 0 0 6px transparent,
    0 12px 26px var(--shade);
}
@keyframes ring {
  50% {
    box-shadow:
      inset 0 1px 0 var(--hi),
      inset 0 -1px 0 var(--lo),
      0 0 0 3px var(--field),
      0 0 0 6px var(--a1),
      0 0 18px 8px color-mix(in srgb, var(--a1) 34%, transparent),
      0 12px 26px var(--shade);
  }
}
.row a:focus-visible { outline: none; }
.row a.is-on, .row a:focus-visible, .row a.launch { z-index: 1; }
/* press, and the launch: pressing A on the Switch grows the tile before the
   game takes the screen. The script holds the class for the length of it. */
.row a:active b { --s: 0.96; transition-duration: var(--t-fast); }
.row a.launch b { --s: 1.2; transition-duration: 0.24s; }

/* The chapter still running is marked, not selected. A ring here read as the
   cursor, which contradicted the title. A small live pip instead. */
.row a[data-state="now"] b::after {
  content: "";
  position: absolute;
  top: 10px;
  right: 10px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--on-hue);
  box-shadow: 0 0 0 3px var(--hue);
  animation: pip 2.4s ease-in-out infinite;
}
@keyframes pip {
  50% { box-shadow: 0 0 0 3px var(--hue), 0 0 0 6px color-mix(in srgb, var(--on-hue) 35%, transparent); }
}

/* With the script the title lives above the row, Switch style. Without it
   every tile carries its own caption, because nothing is tracking focus. */
.cap { font-style: normal; }
.vh,
.js .cap {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- the system row ------------------------------------------------ */
/* Round buttons under the tiles, each a real control, each labelled. */

.apps ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px 22px;
}
.apps a {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 72px;
  padding: 4px 2px;
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.75rem;
  line-height: 1.2;
  -webkit-tap-highlight-color: transparent;
}
.apps a:focus-visible { outline: none; }
.orb {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg, var(--tile-hi), var(--tile) 70%);
  color: var(--ink-2);
  box-shadow:
    inset 0 1px 0 var(--hi),
    inset 0 -1px 0 var(--lo),
    0 2px 6px var(--shade);
  transition:
    transform var(--t) var(--pop),
    color var(--t) var(--out),
    box-shadow var(--t) var(--out);
}
.orb svg {
  width: 26px;
  height: 26px;
  fill: currentColor;
  overflow: visible;
}
/* some buttons sit on a colour, like the Switch's eShop and Album */
.apps a[data-hue] .orb {
  background: var(--hue);
  color: var(--on-hue);
}
.apps a:hover .orb,
.apps a:focus-visible .orb {
  transform: translateY(-3px) scale(1.06);
  color: var(--ink);
}
.apps a[data-hue]:hover .orb,
.apps a[data-hue]:focus-visible .orb {
  color: var(--on-hue);
  box-shadow:
    inset 0 1px 0 var(--hi),
    inset 0 -1px 0 var(--lo),
    0 0 0 3px var(--field),
    0 0 0 5px var(--ink),
    0 8px 16px var(--shade);
}
.apps a:hover,
.apps a:focus-visible { color: var(--ink); }
.apps a:active .orb { transform: translateY(-1px) scale(0.97); transition-duration: var(--t-fast); }

/* ===================== register B: the world map ======================== */

/* The map is a sheet of paper laid on the field: cut edge, offset shadow,
   grain. Going from the home screen onto it is the drop into the game. */
.map {
  position: relative;
  margin: 0 16px;
  padding: 58px 22px 64px;
  background: var(--paper) var(--grain);
  color: var(--plaque-ink);
  border: 2px solid var(--rule);
  border-radius: 30px;
  box-shadow:
    inset 0 2px 0 var(--hi),
    10px 12px 0 var(--drop);
  --hue: var(--a3);
}
.defs { position: absolute; }

/* The route, drawn by the script as one smooth line down the map, with the
   nodes and milestones placed on it. A soft band of worn ground, a line of
   beads on it, and a light on each bead. */
.route {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.route path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.route .road { stroke: var(--trail); stroke-width: 14px; opacity: 0.14; }
.route .beads { stroke: var(--trail); stroke-width: 5px; stroke-dasharray: 0 13; }
.route .beads.lit { stroke: var(--trail-2); stroke-width: 2px; }
/* ahead of where I am the route is not walked yet: the same beads, fading
   out along their length under the mask in the markup */

/* ---------- the title screen ---------------------------------------------- */

.title {
  position: relative;
  max-width: var(--wrap-map);
  margin: 0 auto;
  padding-bottom: 74px;
  display: grid;
  grid-template-columns: 46px 1fr;
}
.title-card {
  grid-column: 1 / -1;
  justify-self: center;
  position: relative;
  text-align: center;
  padding: 28px 46px 30px;
  background: var(--tex), var(--plaque);
  border: 3px solid var(--edge);
  border-radius: 26px;
  box-shadow:
    inset 0 2px 0 var(--hi),
    inset 0 -3px 0 var(--lo),
    8px 10px 0 var(--drop);
}
/* the coloured tab a course banner carries */
.title-card::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 50%;
  width: 64px;
  height: 7px;
  transform: translateX(-50%);
  background: var(--a3);
  border-radius: 0 0 5px 5px;
}
.title h1 {
  font-size: clamp(2.3rem, 1.7rem + 2.4vw, 3.5rem);
  letter-spacing: -0.012em;
}
.title p {
  margin-top: 6px;
  color: var(--plaque-ink-2);
  font-size: clamp(1rem, 0.92rem + 0.4vw, 1.2rem);
  font-weight: 500;
}

/* ---------- the route without the script ---------------------------------- */

/* Beads on a gentle wave, straight down the node column. This is what the map
   shows when the script is not running to draw the real route; the script
   marks the map `routed` and these step aside. Six gradient layers, one bead
   each per period, tile into a route that sways as it goes. */
.ch::before,
.title::before {
  content: "";
  position: absolute;
  left: 23px;
  top: 50px;
  bottom: 0;
  width: 28px;
  transform: translateX(-50%);
  --bead: var(--trail-2) 0, var(--trail) 1.7px, var(--trail) 3.7px, transparent 4.7px;
  background-image:
    radial-gradient(circle at 14px 9px, var(--bead)),
    radial-gradient(circle at 18.3px 27px, var(--bead)),
    radial-gradient(circle at 18.3px 45px, var(--bead)),
    radial-gradient(circle at 14px 63px, var(--bead)),
    radial-gradient(circle at 9.7px 81px, var(--bead)),
    radial-gradient(circle at 9.7px 99px, var(--bead));
  background-size: 28px 108px;
  background-repeat: repeat-y;
}
.title::before {
  top: auto;
  height: 66px;
}
/* ahead of where I am, the route is not walked yet */
.ch[data-state="now"]::before {
  opacity: 0.3;
  mask-image: linear-gradient(to bottom, #000 40%, transparent);
  -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent);
}
.routed .ch::before,
.routed .title::before { display: none; }

/* ---------- chapters ------------------------------------------------------ */

.ch {
  position: relative;
  max-width: var(--wrap-map);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 46px 1fr;
  gap: 0 24px;
  padding-bottom: 54px;
}

.node {
  grid-column: 1;
  grid-row: 1;
  position: relative;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: linear-gradient(180deg, var(--tile-hi), var(--plaque) 70%);
  border: 3px solid var(--edge);
  display: grid;
  place-items: center;
  z-index: 1;
  box-shadow:
    inset 0 2px 0 var(--hi),
    inset 0 -2px 0 var(--lo),
    3px 4px 0 var(--drop);
  transition:
    transform var(--t) var(--pop),
    border-color var(--t) var(--out),
    box-shadow var(--t) var(--out);
}
.ch[data-state="clear"] .node { border-color: var(--hue); }
.ch:hover .node,
.ch:focus-within .node { transform: scale(1.1); }
/* the chapter takes focus on arrival; the plaque shows it, so the section
   itself must not draw a box around the whole row */
.ch:focus, .ch:focus-visible { outline: none; }

.num {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.2rem;
  color: var(--plaque-ink);
}

/* The chapter still running is not cleared, so it gets a ring, not a flag.
   The ring breathes: it is the "you are here" marker. */
.ch[data-state="now"] .node {
  border-color: var(--hue);
  box-shadow:
    inset 0 2px 0 var(--hi),
    inset 0 -2px 0 var(--lo),
    3px 4px 0 var(--drop),
    0 0 0 4px var(--paper),
    0 0 0 7px var(--hue),
    0 0 0 7px transparent;
  animation: here 2.4s ease-in-out infinite;
}
@keyframes here {
  50% {
    box-shadow:
      inset 0 2px 0 var(--hi),
      inset 0 -2px 0 var(--lo),
      3px 4px 0 var(--drop),
      0 0 0 4px var(--paper),
      0 0 0 8px var(--hue),
      0 0 20px 8px color-mix(in srgb, var(--hue) 32%, transparent);
  }
}

/* The pennant. Planted on the side of the node the plaque is not on, which is
   the only place it is neither on the route nor under the card. The pin is
   what gets planted (a scale, once, on reveal) and the flag inside it is what
   sways and flutters (a rotation), so the hover can never restart the
   planting: they are different properties on different elements. */
.pin {
  position: absolute;
  /* below the meander width there is no free side, only the map's edge, so
     the pennant is planted at the node's own shoulder, up and to the left */
  right: 52%;
  top: -25px;
  width: 30px;
  height: 34px;
  transform-origin: 50% 96%;
}
.flag {
  width: 30px;
  height: 34px;
  overflow: visible;
  /* mirrored so the pole stands against the node and the pennant flies out */
  transform: scaleX(-1);
  transform-origin: 50% 96%;
  animation: wave 3.6s ease-in-out infinite;
  color: var(--hue);
  --fsh: var(--drop);
  --fpole: var(--edge);
}
@keyframes wave {
  0%, 100% { rotate: 0deg; }
  50%      { rotate: -4deg; }
}
/* the wave stays in its slot, so it carries on, and the flutter rides on top */
.ch:hover .flag,
.ch:focus-within .flag {
  animation: wave 3.6s ease-in-out infinite, flutter 0.6s var(--out) 1;
}
@keyframes flutter {
  0%   { rotate: 0deg; }
  30%  { rotate: -13deg; }
  65%  { rotate: 6deg; }
  100% { rotate: 0deg; }
}

.plaque {
  grid-column: 2;
  grid-row: 1;
  position: relative;
  background: var(--tex), var(--plaque);
  border: 3px solid var(--edge);
  border-radius: var(--r-plaque);
  padding: 24px 26px;
  box-shadow:
    inset 0 2px 0 var(--hi),
    inset 0 -3px 0 var(--lo),
    var(--sx) var(--sy) 0 var(--drop);
  /* The tilt and the lift ride on custom properties so the hover state sets
     values rather than restating the whole transform. `.ch:nth-of-type(even)
     .plaque` scores (0,3,0) and a bare `.plaque:hover` scores (0,2,0), so the
     resting tilt on the left-hand cards used to outrank the hover rule and only
     the right-hand column lifted. Every state below is matched to (0,3,0) or
     better and ordered after the resting one. */
  --tilt: -0.9deg;
  --lift: 0px;
  --sx: 5px;
  --sy: 6px;
  transform: rotate(var(--tilt)) translateY(var(--lift));
  transition:
    transform var(--t) var(--pop),
    box-shadow var(--t) var(--out),
    border-color var(--t) var(--out);
}
.ch:nth-of-type(even) .plaque { --tilt: 0.9deg; }

/* the wedge that hangs the plaque off the route */
.plaque::after {
  content: "";
  position: absolute;
  top: 11px;
  left: -14px;
  width: 14px;
  height: 24px;
  background: var(--edge);
  clip-path: polygon(100% 0, 0 50%, 100% 100%);
  transition: background var(--t) var(--out);
}

.ch .plaque:hover,
.ch:focus-within .plaque {
  --tilt: 0deg;
  --lift: -4px;
  --sx: 9px;
  --sy: 12px;
  border-color: var(--hue);
}
.ch .plaque:hover::after,
.ch:focus-within .plaque::after { background: var(--hue); }
.ch .plaque:active {
  --lift: -1px;
  --sx: 4px;
  --sy: 5px;
  transition-duration: var(--t-fast);
}

.plaque h2 {
  font-size: clamp(1.4rem, 1.15rem + 1.1vw, 1.95rem);
  margin-bottom: 12px;
}
.plaque p {
  color: var(--plaque-ink);
  max-width: 62ch;
}
.now {
  display: inline-block;
  vertical-align: 0.35em;
  margin-left: 8px;
  padding: 2px 9px;
  border-radius: var(--r-chip);
  background: var(--hue);
  color: var(--on-hue);
  font-family: var(--f-ui);
  font-weight: 700;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
}

/* ---------- reveal -------------------------------------------------------- */
/* Walking down the map, each plaque pops up as it comes into view and its
   pennant plants. The script sets `rv` on chapters below the fold and swaps
   it for `pop` when they arrive; without the script nothing is ever hidden.
   Every animation here fills backwards only: once it has run, the element is
   back on its ordinary styles, so the hover states work and nothing can be
   re-run by a later state change. `pop` is set once and never removed. */
.js .rv .plaque,
.js .rv .title-card,
.js .rv .mile,
.js .rv .deco,
.js .rv.close > * { opacity: 0; }
.js .rv .pin { scale: 0; }
.pop .plaque { animation: plaque-in 0.6s var(--pop) backwards; }
.pop .title-card { animation: card-in 0.6s var(--pop) backwards; }
.pop .mile { animation: card-in 0.5s var(--pop) 0.2s backwards; }
.pop .deco { animation: card-in 0.8s var(--out) 0.15s backwards; }
.pop.close > * { animation: card-in 0.55s var(--pop) backwards; }
.pop .pin { animation: plant 0.5s var(--pop) 0.28s backwards; }
.pop .node { animation: node-in 0.5s var(--pop) backwards; }
@keyframes plaque-in {
  from { opacity: 0; transform: rotate(var(--tilt)) translateY(22px) scale(0.94); }
  to   { opacity: 1; transform: rotate(var(--tilt)) translateY(var(--lift)); }
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(18px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes node-in {
  from { transform: scale(0.6); }
  to   { transform: scale(1); }
}
@keyframes plant {
  from { scale: 0; }
  to   { scale: 1; }
}

/* ---------- chips --------------------------------------------------------- */
/* They are labels. A label does not need a picture of itself. */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 18px;
}
.chips li {
  padding: 5px 14px;
  border: 2px solid var(--edge);
  border-radius: var(--r-chip);
  background: var(--plaque);
  box-shadow: 0 2px 0 var(--drop);
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--plaque-ink);
  white-space: nowrap;
  transition:
    transform var(--t) var(--pop),
    border-color var(--t) var(--out),
    box-shadow var(--t) var(--out);
}
.chips li:hover {
  transform: translateY(-2px);
  border-color: var(--hue);
  box-shadow: 0 4px 0 var(--drop);
}
.chips a { text-decoration: none; }

/* ---------- milestones ---------------------------------------------------- */
/* What each chapter unlocked, as a small stop on the route between its node
   and the next: a badge on the route, on a plate that points away from it.
   Below the meander width it sits under the plaque, a little right of the
   node column, so the route still sways on its way to the next node. */

.mile {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: start;
  position: relative;
  z-index: 1;
  left: var(--bx, 0px);
  margin: 14px 0 0 13px;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 3px 13px 3px 3px;
  border: 2px solid var(--edge);
  border-radius: var(--r-chip);
  background: var(--tex), var(--plaque);
  box-shadow: 2px 3px 0 var(--drop);
  color: var(--plaque-ink-2);
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  transition:
    transform var(--t) var(--pop),
    border-color var(--t) var(--out),
    box-shadow var(--t) var(--out),
    color var(--t) var(--out);
}
.mile:hover {
  transform: translateY(-2px);
  border-color: var(--hue);
  box-shadow: 2px 5px 0 var(--drop);
  color: var(--plaque-ink);
}
.badge {
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg, var(--tile-hi), var(--plaque) 70%);
  border: 2px solid var(--edge);
  box-shadow: inset 0 1px 0 var(--hi), inset 0 -1px 0 var(--lo);
  color: inherit;
  transition: border-color var(--t) var(--out);
}
.badge svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
  overflow: visible;
}
.mile:hover .badge { border-color: var(--hue); }

/* ---------- scenery ------------------------------------------------------- */
/* A few glyphs from the chapter's own vocabulary on the free side of the
   row, the way a map has trees and rocks off the path: grey, small, tilted,
   never in the way of the plate or the route. Each slot is a spot in the
   free column, measured from its outer edge; the same slots mirror on the
   even rows. Hidden where there is no free column. */
.deco {
  display: none;
  grid-row: 1;
  align-self: stretch;
  position: relative;
  pointer-events: none;
  color: var(--trail);
}
.deco li {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--s);
  height: var(--s);
  rotate: var(--r);
}
.deco svg { width: 100%; height: 100%; fill: currentColor; overflow: visible; }
.deco .s1 { --x: 6%;  --y: 6%;  --s: 38px; --r: -8deg; }
.deco .s2 { --x: 38%; --y: 3%;  --s: 28px; --r: 10deg; }
.deco .s3 { --x: 12%; --y: 44%; --s: 52px; --r: 5deg; }
.deco .s4 { --x: 32%; --y: 72%; --s: 34px; --r: -12deg; }
.deco .s5 { --x: 60%; --y: 86%; --s: 26px; --r: 8deg; }
.deco .s6 { --x: 66%; --y: 10%; --s: 30px; --r: -5deg; }
.deco .s7 { --x: 24%; --y: 22%; --s: 24px; --r: 14deg; }
.deco .s8 { --x: 48%; --y: 48%; --s: 36px; --r: -16deg; }

/* ---------- post-it notes ------------------------------------------------- */
/* The homelab's four boxes, one note each, stuck to the plaque. Each note
   sits at its own small angle, has the darker strip a pad leaves at the top,
   and a corner that curls. Pointing at one straightens it. */

.notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px 20px;
  margin-top: 26px;
  padding: 4px 6px 8px 2px;
}
.note {
  position: relative;
  padding: 16px 18px 24px;
  /* The paper is painted in pieces that leave the corner square empty except
     for the flap, so the cut is a real hole: the plaque shows through it and
     the shadow, a filter on the painted shape rather than a box, is cut too. */
  --c: 22px;
  --flap: color-mix(in srgb, var(--paper-note) 84%, #000);
  background:
    linear-gradient(180deg, rgb(0 0 0 / 0.07) 0 17px, transparent 17px),
    linear-gradient(to top left, transparent calc(50% - 0.5px), var(--flap) calc(50% + 0.5px))
      no-repeat 100% 100% / var(--c) var(--c),
    linear-gradient(var(--paper-note), var(--paper-note)) no-repeat 0 0 / 100% calc(100% - var(--c)),
    linear-gradient(var(--paper-note), var(--paper-note)) no-repeat 0 100% / calc(100% - var(--c)) 100%;
  color: var(--note-ink);
  border-radius: 2px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.25);
  filter:
    drop-shadow(4px 5px 0 var(--drop))
    drop-shadow(0 10px 9px rgb(0 0 0 / 0.22));
  --r: 0deg;
  transform: rotate(var(--r));
  transition: transform var(--t) var(--pop), filter var(--t) var(--out);
}
.note[data-note="a"] { --paper-note: var(--note-a); --r: -1.7deg; }
.note[data-note="b"] { --paper-note: var(--note-b); --r: 1.3deg; }
.note[data-note="c"] { --paper-note: var(--note-c); --r: -0.9deg; }
.note[data-note="d"] { --paper-note: var(--note-d); --r: 2.1deg; }
.note:hover {
  transform: rotate(0deg) translateY(-4px) scale(1.02);
  filter:
    drop-shadow(7px 9px 0 var(--drop))
    drop-shadow(0 18px 13px rgb(0 0 0 / 0.28));
  z-index: 1;
}
.note h3 {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.15rem;
  line-height: 1.2;
  letter-spacing: -0.005em;
  margin: 0;
}
.note .where {
  color: var(--note-ink);
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.72;
  margin: 1px 0 9px;
}
.note ul {
  font-family: var(--f-display);
  font-weight: 400;
  font-size: 0.9375rem;
  line-height: 1.45;
}
.note ul li {
  position: relative;
  padding-left: 15px;
}
.note ul li::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}

/* ---------- the close ----------------------------------------------------- */

.close {
  max-width: var(--wrap-map);
  margin: 0 auto;
  padding: 14px 0 0 70px;
}
.close h2 {
  font-size: clamp(1.7rem, 1.25rem + 1.7vw, 2.4rem);
  margin-bottom: 14px;
}
.close p, .close blockquote { max-width: 62ch; }
.close blockquote {
  margin: 30px 0 0;
  padding: 4px 0 4px 22px;
  border-left: 5px solid var(--a3);
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.32rem);
  line-height: 1.55;
}
.close .chips { margin-top: 18px; }

/* ---------- footer -------------------------------------------------------- */

.foot {
  max-width: var(--wrap-meander);
  margin: 44px auto 0;
  padding: 0 22px 44px;
  color: var(--ink-2);
  font-size: 0.8125rem;
  line-height: 1.65;
  text-align: center;
}
.foot b { color: var(--ink); }
.foot p { max-width: 62ch; margin: 0 auto; }

/* ===================== viewport =========================================== */

/* Wide enough for the whole row. It stops being a scroller: a scroller clips
   whatever a tile paints outside itself, and hides the last tile behind a
   scrollbar nobody can see. The seven columns shrink together when the frame
   is narrower than the tiles want, so every tile is always on screen. */
@media (min-width: 900px) {
  .deck { width: fit-content; max-width: 100%; margin-inline: auto; }
  .row { overflow: visible; scroll-snap-type: none; margin-inline: 0; padding-inline: 0; }
  .row ul {
    grid-auto-flow: row;
    grid-template-columns: repeat(7, minmax(0, 122px));
  }
}

/* Wide enough to meander. The plaques alternate either side of the route so the
   chapters read as a map rather than as a vertical timeline, which is a
   different thing wearing similar dots. */
@media (min-width: 900px) {
  .ch, .title {
    max-width: var(--wrap-meander);
    grid-template-columns: 1fr 46px 1fr;
    gap: 0 32px;
  }
  .ch::before, .title::before { left: 50%; }
  .node { grid-column: 2; }
  .plaque { grid-column: 3; }
  .ch:nth-of-type(even) .plaque { grid-column: 1; }
  /* the plaque swaps sides on even chapters, so the flag, the wedge and the
     milestone swap with it */
  .pin { right: 100%; top: -21px; margin-right: 3px; }
  .ch:nth-of-type(even) .pin {
    right: auto;
    left: 100%;
    margin: 0 0 0 3px;
  }
  .ch:nth-of-type(even) .flag { transform: none; }
  /* The milestone sits on the route: its badge is pulled onto the centre
     line by a negative margin (half the node, the column gap, the badge's
     own edge), and the script then slides it, and where the map has room
     the whole row, to wherever the line actually runs (`--bx`, `--sx`). The
     plate reaches into the free column. */
  .ch { --gap: 32px; left: var(--sx, 0px); }
  .close { position: relative; left: var(--sx, 0px); }
  .mile {
    grid-row: 1;
    align-self: center;
    margin: 0;
  }
  .ch:nth-of-type(odd) .mile {
    grid-column: 1;
    justify-self: end;
    flex-direction: row-reverse;
    padding: 3px 3px 3px 13px;
    margin-right: calc(-43px - var(--gap));
  }
  .ch:nth-of-type(even) .mile {
    grid-column: 3;
    justify-self: start;
    margin-left: calc(-43px - var(--gap));
  }
  .deco { display: block; grid-column: 1; }
  .ch:nth-of-type(even) .deco { grid-column: 3; }
  .ch:nth-of-type(even) .deco li { left: auto; right: var(--x); }
  .ch:nth-of-type(even) .plaque::after {
    left: auto;
    right: -14px;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
  }
  /* These align with the right-hand plaque column by sharing the chapter grid,
     NOT by a percentage padding. A percentage padding resolves against the
     containing block, not against the element's own max-width, so on a wide
     monitor it grew past the 1180px cap and crushed the closing block into a
     slim column. At 2560 it collapsed the content to nothing. */
  .close {
    max-width: var(--wrap-meander);
    display: grid;
    grid-template-columns: 1fr 46px 1fr;
    gap: 0 32px;
    padding-left: 0;
  }
  .close > * { grid-column: 3; }
}

/* On a large monitor a 1180px cap left the page a ribbon with 700px of dead
   space either side. The measure stays honest; the frame around it grows. */
@media (min-width: 1600px) {
  :root {
    --wrap-home: 1420px;
    --wrap-meander: 1420px;
    --bar: 64px;
  }
  .row ul { grid-template-columns: repeat(7, minmax(0, 142px)); gap: 18px; }
  .row a b .n { font-size: 1.1rem; top: 10px; left: 13px; }
  .row-title { font-size: 1.7rem; }
  .row-teaser { font-size: 1rem; }
  .orb { width: 58px; height: 58px; }
  .ch, .title, .close { gap: 0 44px; }
  .ch { --gap: 44px; }
  .plaque { padding: 28px 32px; }
  .map { margin: 0 24px; }
}

@media (min-width: 2200px) {
  :root {
    --wrap-home: 1700px;
    --wrap-meander: 1640px;
  }
  .row ul { grid-template-columns: repeat(7, minmax(0, 164px)); gap: 22px; }
  .row a b .n { font-size: 1.2rem; }
  .row-title { font-size: 1.9rem; }
  .row-teaser { font-size: 1.0625rem; }
  .plaque { padding: 32px 36px; }
  .map { margin: 0 32px; }
}

@media (max-width: 620px) {
  /* On a phone the slack between the row and the system row is most of the
     screen, so the home screen stops filling the viewport and the map starts
     where the content ends. */
  .home {
    gap: 22px;
    min-height: auto;
    padding: 26px 20px 30px;
  }
  .deck { gap: 22px; }
  .plate { min-height: 0; }
  .row { margin-inline: -20px; padding-inline: 20px; }
  .row ul { grid-auto-columns: 104px; padding: 22px 10px 24px; margin: -10px; }
  .row a b .n { font-size: 0.85rem; top: 7px; left: 9px; }
  .row-title { font-size: 1.3rem; }
  .apps ul { gap: 4px 5px; }
  .apps a { min-width: 56px; max-width: 66px; padding: 4px 0; }
  .lbl { white-space: normal; text-align: center; line-height: 1.2; }
  .orb { width: 48px; height: 48px; }
  .orb svg { width: 23px; height: 23px; }
  .bar { padding: 0 20px; gap: 10px; }
  .tz-long { display: none; }
  .tz-short { display: inline; }
  /* no keyboard on a phone, so no key legend: a legend that is not true is
     worse than none */
  .js .hints { display: none; }
  .js body { padding-bottom: 0; }
  .map {
    margin: 0 8px;
    padding: 40px 16px 44px;
    border-radius: 20px;
    box-shadow: inset 0 2px 0 var(--hi), 6px 7px 0 var(--drop);
  }
  .title { padding-bottom: 58px; grid-template-columns: 38px 1fr; }
  .title-card { padding: 20px 22px 22px; border-radius: 20px; border-width: 2px; }
  .title::before { height: 52px; }
  .ch { grid-template-columns: 38px 1fr; gap: 0 14px; padding-bottom: 40px; }
  .ch::before, .title::before {
    left: 19px;
    width: 20px;
    --bead: var(--trail-2) 0, var(--trail) 1.5px, var(--trail) 3.2px, transparent 4.2px;
    background-image:
      radial-gradient(circle at 10px 8px, var(--bead)),
      radial-gradient(circle at 13px 24px, var(--bead)),
      radial-gradient(circle at 13px 40px, var(--bead)),
      radial-gradient(circle at 10px 56px, var(--bead)),
      radial-gradient(circle at 7px 72px, var(--bead)),
      radial-gradient(circle at 7px 88px, var(--bead));
    background-size: 20px 96px;
  }
  .ch::before { top: 42px; }
  .node { width: 38px; height: 38px; border-width: 2px; }
  .num { font-size: 1rem; }
  .pin { width: 25px; height: 28px; top: -22px; }
  .flag { width: 25px; height: 28px; }
  .mile { margin: 12px 0 0 9px; }
  .badge { width: 26px; height: 26px; }
  .badge svg { width: 14px; height: 14px; }
  .plaque { padding: 19px 19px; border-radius: 16px; border-width: 2px; }
  .plaque::after { top: 9px; left: -12px; width: 12px; height: 20px; }
  .notes { grid-template-columns: 1fr; gap: 18px; margin-top: 22px; }
  .note { padding: 14px 16px 22px; }
  .close { padding-left: 0; }
  .foot { margin-top: 36px; padding: 0 20px 40px; }
}

/* a touch screen has no keys either */
@media (hover: none) and (pointer: coarse) {
  .js .hints { display: none; }
  .js body { padding-bottom: 0; }
}

/* ---------- preferences --------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation: none !important;
  }
  /* State still has to be visible without the movement that usually carries it,
     so the ring, the colour and the lift stay; only the travel goes. */
  .row a:hover b,
  .row a:active b,
  .row a.launch b { --ty: 0px; --s: 1; }
  .row a.is-on b,
  .row a:focus-visible b { --s: 1; }
  .row a:hover b .art { transform: none; }
  .apps a:hover .orb, .apps a:focus-visible .orb, .apps a:active .orb { transform: none; }
  .plaque, .ch .plaque:hover, .ch:focus-within .plaque { --tilt: 0deg; --lift: 0px; }
  .chips li:hover { transform: none; }
  .mile:hover { transform: none; }
  .note, .note:hover { transform: none; }
  .ch:hover .node, .ch:focus-within .node { transform: none; }
  .plate.out { opacity: 1; transform: none; }
  /* nothing may stay hidden behind a reveal that will never run */
  .js .rv .plaque, .js .rv .title-card, .js .rv .mile, .js .rv .deco, .js .rv.close > * { opacity: 1; }
  .js .rv .pin { scale: 1; }
}

@media print {
  .bar, .home, .hints { display: none; }
  .js body { padding-bottom: 0; }
  .map { background: none; padding: 0; margin: 0; border: 0; box-shadow: none; }
  .plaque, .title-card, .note, .mile { box-shadow: none; filter: none; transform: none; }
  .route { display: none; }
  .js .rv .plaque, .js .rv .title-card, .js .rv .mile, .js .rv .deco, .js .rv.close > * { opacity: 1; }
  .js .rv .pin { scale: 1; }
}

/* ---------- not found ----------------------------------------------------- */
/* The route runs out: a node with nothing in it, and the beads fading to
   nothing after it. */
.lost { margin-top: 40px; min-height: 50vh; }
.lost .ch { padding-top: 10px; }
.lost .ch::before {
  top: auto;
  bottom: 0;
  height: 120px;
  opacity: 0.3;
  mask-image: linear-gradient(to bottom, #000, transparent);
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent);
}
.lost .node { border-style: dashed; }
