/* celyrian.com
 *
 * Two registers inside one frame. The frame (the bar, the sides, the hint
 * drawer) stays put the way a system UI does; the home screen and the map
 * scroll through its rounded cut-out.
 *
 * 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, and scenery off the path.
 *
 * The finish is Celyrian Wonder v2, the desktop's design system: dark first,
 * opaque surfaces, big surfaces seamless (one colour, one soft shadow, no edge
 * line), the hard edge and offset drop only on small things (tiles, orbs,
 * chips, keycaps), patterns only on the background, rose only for errors.
 * Colours, radii, motion and fonts come from tokens.css, which is generated
 * from the palette (tools/tokens.py); nothing below declares a colour.
 *
 * Nothing here is a Nintendo asset. The grammar is copied, the artwork is not.
 *
 * Rules this file keeps:
 *   - no hex below the alias block: every colour is a token
 *   - 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 Gothic New";
  src: url("/fonts/zen-regular.4e6a2358.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku Gothic New";
  src: url("/fonts/zen-medium.9d2cf4c5.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku Gothic New";
  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, the shapes, the curves and the fonts are in tokens.css, loaded
   before this file. These are the site's own names for a few of them, and the
   layout sizes. */
:root {
  --on-hue: var(--on-accent);
  --plaque-ink: var(--ink);
  --r-plaque: var(--r-panel);
  --r-chip: var(--r-pill);
  /* the light on a bead, and the band of light the selected tile sweeps */
  --trail-2: rgb(255 255 255 / 0.22);
  --hi: rgb(255 255 255 / 0.07);
  --lift-shadow: 0 12px 34px rgb(0 0 0 / 0.6);

  --bar: 58px;
  --hint: 46px;
  --rim: 10px;
  --peek: 76px;
  --wrap-home: 1180px;
  --wrap-map: 780px;
  --wrap-meander: 1180px;
}
[data-theme="light"] {
  --trail-2: rgb(255 255 255 / 0.8);
  --hi: rgb(255 255 255 / 0.6);
  --lift-shadow: 0 12px 30px rgb(60 40 50 / 0.2);
}

/* 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="e"] { --hue: var(--a5); }
/* rose (a4) is not a chapter colour: it means an error and nothing else */

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

* { box-sizing: border-box; }

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

/* The page behind everything: the deepest colour and the sprinkle pattern,
   fixed, so the pattern never scrolls with the content and never lands on a
   panel. Patterns live here and nowhere else. */
html { background: var(--paper); }
body {
  margin: 0;
  background: var(--paper) var(--pat-sprinkle) fixed;
  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: calc(var(--hint) + var(--rim)); }

/* The frame. One colour, one soft shadow cast inward, no edge line: the bar
   is its top, a thin rim its sides and bottom, and the page scrolls through
   the rounded cut-out in the middle, the way the desktop's shell frames its
   windows. Drawn by one fixed box whose spread shadow is the frame itself, so
   nothing under it can be clicked by accident: it takes no pointer events. */
body::after {
  content: "";
  position: fixed;
  inset: var(--bar) var(--rim) var(--rim);
  z-index: 9;
  border-radius: var(--r-panel);
  box-shadow: 0 0 0 100vmax var(--field), inset 0 2px 24px rgb(0 0 0 / 0.5);
  pointer-events: none;
}
[data-theme="light"] body::after { box-shadow: 0 0 0 100vmax var(--field), inset 0 2px 20px rgb(60 40 50 / 0.12); }

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

.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-display);
  font-weight: 600;
  font-size: 1.2rem;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}
.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(--tile-hi);
  border: 1px solid var(--rule);
  padding: 2px;
  display: block;
  transition: background var(--t) var(--out);
}
.pill-knob {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--ink-2);
  display: block;
  transition: transform var(--t) var(--pop), background var(--t) var(--out);
}
.pill[aria-pressed="true"] .pill-track { background: var(--a1); border-color: var(--a1); }
.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 drawer. Fixed, because the keys it names work anywhere on the page,
   and a legend that scrolls away stops being a legend. It grows out of the
   frame's bottom edge on the right, the way the Switch draws its button
   prompts, and joins the frame with two concave fillets, so it reads as part
   of the frame and not as a box laid on top of it. */
.hints {
  display: none;
  position: fixed;
  right: 60px;
  bottom: 0;
  z-index: 10;
  height: var(--hint);
  align-items: center;
  gap: 24px;
  padding: 2px 22px 0;
  background: var(--field);
  border-radius: var(--r-panel) var(--r-panel) 0 0;
  color: var(--ink-2);
  font-size: 0.8125rem;
}
.hints::before,
.hints::after {
  content: "";
  position: absolute;
  bottom: var(--rim);
  width: var(--r-panel);
  height: var(--r-panel);
  pointer-events: none;
}
.hints::before {
  right: 100%;
  background: radial-gradient(circle at 0 0, transparent calc(var(--r-panel) - 0.5px), var(--field) var(--r-panel));
}
.hints::after {
  left: 100%;
  background: radial-gradient(circle at 100% 0, transparent calc(var(--r-panel) - 0.5px), var(--field) var(--r-panel));
}
.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;
  border: 1px solid var(--rule);
  box-shadow: 0 2px 0 var(--drop);
  transition: transform var(--t-fast) var(--out), box-shadow var(--t-fast) var(--out), background var(--t) var(--out), border-color var(--t) var(--out);
}
.key:hover kbd { background: var(--rule); border-color: var(--edge); }
.key:active kbd { transform: translateY(2px); box-shadow: 0 0 0 var(--drop); }
.hints span .key + .key { margin-left: 3px; }

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

/* From the width where the whole row fits it is sized so the map shows at
   the bottom edge (see the viewport rules): a full-height screen with nothing
   under it gives the reader no reason to believe there is more. Narrower than
   that the row is a scroller and the slack between it and the system row would
   be most of the screen, so the home screen ends where its content does. */
.home {
  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; }

/* the one line above the grid on a phone; the plate replaces it from the
   width where there is a cursor to describe */
.intro {
  display: none;
  padding-left: 2px;
  font-family: var(--f-display);
  font-weight: 500;
  font-size: 1.25rem;
  line-height: 1.3;
  color: var(--ink);
  max-width: 24ch;
}
.intro-kick {
  display: block;
  margin-bottom: 4px;
  font-family: var(--f-mono);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.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;
  /* The ring, its glow and the lift shadow are all painted outside the tile
     box, and a scroller clips at its own padding box, so the slack they need
     is padding on the scroller itself: 30px above for the glow, 42px below
     for the shadow. The margins take the same amount back, so the deck keeps
     its rhythm. Sideways it runs under the home's own padding, edge to edge,
     so the tiles scroll out under the screen edge rather than under a border. */
  margin: -16px -22px -26px;
  padding: 30px 0 42px;
  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;
  /* Sized to its tracks, so its own padding is part of the scroll range at
     both ends. A scroller does not reliably count its end padding (the tracks
     used to overflow the list, and the last tile stopped flush with the
     screen edge), so the slack the first and the last tile need lives here. */
  width: max-content;
  padding: 0 22px;
}

.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: flat, opaque,
     and the hard offset drop that small things carry */
  background: var(--hue);
  box-shadow: var(--drop-chip);
  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: hover lifts 2px onto a deeper drop, press sinks onto a shallow one */
.row a:hover b { --ty: var(--hover-lift); box-shadow: var(--drop-hover); }
.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: var(--select-scale);
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 6px var(--ink);
  animation: ring var(--t-breathe) ease-in-out infinite;
}
/* the token's breathe, with the gap in the colour the tiles stand on */
@keyframes ring {
  50% {
    box-shadow:
      0 0 0 3px var(--paper),
      0 0 0 6px var(--a1),
      0 0 14px 6px color-mix(in srgb, var(--a1) 34%, transparent);
  }
}
.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: var(--press-scale); --ty: 1px; box-shadow: var(--drop-press); 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: var(--tile);
  border: var(--edge-chip) solid var(--edge);
  color: var(--ink);
  box-shadow: var(--drop-chip);
  transition:
    transform var(--t) var(--pop),
    color var(--t) var(--out),
    border-color var(--t) var(--out),
    box-shadow var(--t) var(--out);
}
.orb svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
  overflow: visible;
}
:where(.apps a) { --hue: var(--a1); }
.apps a:hover .orb,
.apps a:focus-visible .orb {
  transform: translateY(var(--hover-lift));
  border-color: var(--hue);
  color: var(--hue);
  box-shadow: var(--drop-hover);
}
.apps a:focus-visible .orb { box-shadow: var(--drop-hover), 0 0 0 3px var(--paper), 0 0 0 6px var(--focus); }
.apps a:hover,
.apps a:focus-visible { color: var(--ink); }
.apps a:active .orb { transform: translateY(1px) scale(var(--press-scale)); box-shadow: var(--drop-press); transition-duration: var(--t-fast); }

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

/* The map is one big seamless surface: a single colour, one soft shadow, no
   edge line and no texture. Going from the home screen onto it is the drop
   into the game. */
.map {
  position: relative;
  margin: 0 22px;
  padding: 58px 22px 64px;
  background: var(--plaque);
  color: var(--plaque-ink);
  border-radius: 30px;
  box-shadow: var(--soft-shadow);
  --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: 30px 48px 32px;
  background: var(--tile);
  border-radius: var(--r-panel);
  box-shadow: var(--soft-shadow);
}
/* the coloured tab a course banner carries */
.title-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 64px;
  height: 6px;
  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;
  /* a hash jump lands under the sticky bar, not behind it */
  scroll-margin-top: calc(var(--bar) + 24px);
}
.close { scroll-margin-top: calc(var(--bar) + 24px); }

.node {
  grid-column: 1;
  grid-row: 1;
  position: relative;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: var(--tile);
  border: 3px solid var(--edge);
  display: grid;
  place-items: center;
  z-index: 1;
  box-shadow: var(--drop-chip);
  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); }
/* the node and the plaque are stops: pressing one moves the cursor there */
.node, .plaque { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.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:
    var(--drop-chip),
    0 0 0 4px var(--plaque),
    0 0 0 7px var(--hue),
    0 0 0 7px transparent;
  animation: here 2.4s ease-in-out infinite;
}
@keyframes here {
  50% {
    box-shadow:
      var(--drop-chip),
      0 0 0 4px var(--plaque),
      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(--tile);
  border-radius: var(--r-plaque);
  padding: 26px 28px 26px;
  box-shadow: var(--soft-shadow);
  /* 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;
  transform: rotate(var(--tilt)) translateY(var(--lift));
  transition:
    transform var(--t) var(--pop),
    box-shadow var(--t) var(--out);
}
/* The chapter's colour, as the tab a course banner carries. It widens when
   the plaque is pointed at, the way the edge used to take the colour. */
.plaque::before {
  content: "";
  position: absolute;
  top: 0;
  left: 28px;
  width: 48px;
  height: 6px;
  background: var(--hue);
  border-radius: 0 0 5px 5px;
  transition: width var(--t) var(--pop);
}
.ch:nth-of-type(even) .plaque { --tilt: 0.9deg; }

/* the wedge that hangs the plaque off the route: the plaque's own colour, so
   card and wedge are one shape */
.plaque::after {
  content: "";
  position: absolute;
  top: 14px;
  left: -13px;
  width: 14px;
  height: 24px;
  background: var(--tile);
  clip-path: polygon(100% 0, 0 50%, 100% 100%);
}

.ch .plaque:hover,
.ch:focus-within .plaque {
  --tilt: 0deg;
  --lift: -4px;
  box-shadow: var(--lift-shadow);
}
.ch .plaque:hover::before,
.ch:focus-within .plaque::before { width: 76px; }
.ch .plaque:active {
  --lift: -1px;
  box-shadow: var(--soft-shadow);
  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: var(--edge-chip) solid var(--edge);
  border-radius: var(--r-chip);
  background: var(--plaque);
  box-shadow: var(--drop-chip);
  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(var(--hover-lift));
  border-color: var(--hue);
  box-shadow: var(--drop-hover);
}
.chips li:active { transform: translateY(1px) scale(var(--press-scale)); box-shadow: var(--drop-press); transition-duration: var(--t-fast); }
.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: var(--edge-chip) solid var(--edge);
  border-radius: var(--r-chip);
  background: var(--tile);
  box-shadow: var(--drop-chip);
  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(var(--hover-lift));
  border-color: var(--hue);
  box-shadow: var(--drop-hover);
  color: var(--plaque-ink);
}
.badge {
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--plaque);
  border: var(--edge-chip) solid var(--edge);
  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 boxes, one note each, stuck to the plaque. Dark notes, the
   way the desktop draws them: a charcoal face, a thin strip of the note's
   colour along the top, a folded corner in the same colour, and a label in
   it. The colour says what kind of note it is (lemon, green, sky); pink would
   mean something is wrong, so it is not used here. Pointing at a note
   straightens it and lifts it onto a deeper drop. */

.notes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
  margin-top: 24px;
  padding: 2px 4px 6px 2px;
}
.note {
  position: relative;
  padding: 17px 18px 15px;
  background: color-mix(in srgb, var(--plaque) 94%, var(--h));
  border: var(--edge-chip) solid color-mix(in srgb, var(--edge) 65%, var(--h));
  border-radius: var(--r-note);
  box-shadow: var(--drop-chip);
  --r: 0deg;
  transform: rotate(var(--r));
  transition: transform var(--t) var(--pop), box-shadow var(--t) var(--out);
}
.note::before {
  content: "";
  position: absolute;
  left: 10px;
  right: 26px;
  top: 3px;
  height: 4px;
  border-radius: 2px;
  background: var(--h);
}
/* the fold: the corner turned down, in the note's colour, with the plaque
   showing where the paper was */
.note::after {
  content: "";
  position: absolute;
  right: -2px;
  top: -2px;
  width: 16px;
  height: 16px;
  background: linear-gradient(225deg, var(--tile) 50%, var(--h) 50%);
  border-bottom-left-radius: 3px;
}
.note[data-note="lemon"] { --h: var(--note-lemon); --r: -1deg; }
.note[data-note="green"] { --h: var(--note-green); --r: 0.8deg; }
.note[data-note="sky"]   { --h: var(--note-sky);   --r: -0.6deg; }
.note:hover {
  transform: rotate(0deg) translateY(var(--hover-lift));
  box-shadow: var(--drop-hover);
  z-index: 1;
}
.note .where {
  color: var(--h);
  font-family: var(--f-ui);
  font-weight: 700;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.4;
  margin: 2px 0 3px;
}
.note h3 {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.1rem;
  line-height: 1.2;
  letter-spacing: -0.005em;
  margin: 0 0 8px;
  color: var(--plaque-ink);
}
.note ul {
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--plaque-ink-2);
}
.note ul li {
  position: relative;
  padding-left: 14px;
}
/* on a light page a pastel label would vanish: it takes some ink */
[data-theme="light"] .note { --h-ink: color-mix(in srgb, var(--h) 35%, var(--ink)); }
[data-theme="light"] .note .where { color: var(--h-ink); }
[data-theme="light"] .note ul li::before { background: var(--h-ink); opacity: 0.5; }
.note ul li::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 0.62em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--h);
  opacity: 0.8;
}

/* ---------- 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) {
  .home { min-height: calc(100svh - var(--bar) - var(--peek)); }
  .deck { width: fit-content; max-width: 100%; margin-inline: auto; }
  /* nothing clips any more, so the slack is ordinary spacing: the same 14px
     above and 16px below the tiles the deck has always had */
  .row { overflow: visible; scroll-snap-type: none; margin: 0; padding: 14px 0 16px; }
  .row ul {
    grid-auto-flow: row;
    grid-template-columns: repeat(7, minmax(0, 122px));
    width: auto;
    padding: 0;
  }
}

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

/* Below the meander width the sign is centred and the node column is at the
   left edge, so the route needs room under the sign to bend across. */
@media (max-width: 899px) {
  .title { padding-bottom: 104px; }
  .title::before { height: 96px; }
}

/* ===================== the phone ======================================== */
/* Below 720px the page is laid out for a hand, not shrunk from a desk. The
   home screen is a grid, the way a phone's own home screen is: every chapter
   on screen at once, a caption under each tile, nothing scrolling sideways,
   and no focused title above the row, because a thumb has no cursor. The
   map runs down the middle: the route on the centre line, every node on it,
   the plaque full-width underneath with its wedge pointing up at the node,
   and the milestone on the route below, swinging the line to one side and
   then the other. The pennant is planted beside the node, where there is
   room now. Nothing is tilted: on a narrow page a tilted card reads as a
   crooked one. */
@media (max-width: 720px) {
  .home { gap: 22px; padding: 22px 20px 30px; }
  .deck { gap: 18px; }
  .intro { display: block; }
  .js .plate { display: none; }

  /* the grid */
  .row { overflow: visible; margin: 0; padding: 0; scroll-snap-type: none; }
  .row ul {
    grid-auto-flow: row;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-auto-columns: auto;
    gap: 16px 12px;
    width: auto;
    padding: 0;
  }
  .row li { scroll-snap-align: none; }
  .row a { gap: 7px; font-size: 0.75rem; }
  .row a b { border-radius: 12px; }
  .row a b .n { font-size: 0.8rem; top: 6px; left: 8px; }
  .row a[data-state="now"] b::after { top: 7px; right: 7px; width: 8px; height: 8px; }
  /* the caption is the title here, script or not */
  .cap, .js .cap {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    text-align: center;
    line-height: 1.25;
    color: var(--ink-2);
  }
  .row a.is-on .cap { color: var(--ink); }
  .row a.is-on b, .row a:focus-visible b { --s: 1.05; }

  .apps ul { gap: 6px 4px; }
  .apps a { min-width: 60px; max-width: 76px; padding: 4px 0; font-size: 0.7rem; }
  .lbl { white-space: normal; text-align: center; line-height: 1.2; }
  .orb { width: 50px; height: 50px; }
  .orb svg { width: 24px; height: 24px; }

  .bar { padding: 0 20px; gap: 10px; }
  /* a phone's own screen is the frame: the bar stays, the sides go, and only
     the top corners of the cut-out are rounded */
  :root { --rim: 0px; }
  body::after { border-radius: var(--r-panel) var(--r-panel) 0 0; }
  .clock { font-size: 1.05rem; }
  .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; }

  /* the map, down the middle */
  .map {
    margin: 0 8px;
    padding: 34px 14px 40px;
    border-radius: 20px;
  }
  .title { grid-template-columns: 1fr; padding-bottom: 60px; }
  .title-card { padding: 22px 22px 22px; }
  .ch { grid-template-columns: 1fr; gap: 0; padding-bottom: 42px; justify-items: center; }
  .ch::before, .title::before {
    left: 50%;
    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;
  }
  .title::before { height: 54px; }
  .ch::before { top: 44px; }
  .node { grid-column: 1; grid-row: 1; justify-self: center; width: 44px; height: 44px; border-width: 3px; }
  .num { font-size: 1.1rem; }
  /* The pennant stands on the side the route does not arrive from: the
     odd rows' milestones are left of the line, so the route reaches an even
     node from the left and its flag goes right, and the other way round. */
  .pin { right: 100%; top: -13px; margin-right: 1px; width: 26px; height: 30px; }
  .ch:nth-of-type(even) .pin { right: auto; left: 100%; margin: 0 0 0 1px; }
  .ch:nth-of-type(even) .flag { transform: none; }
  .flag { width: 26px; height: 30px; }
  .plaque {
    grid-column: 1;
    grid-row: 2;
    width: 100%;
    margin-top: 16px;
    padding: 22px 20px 22px;
    --tilt: 0deg;
  }
  .ch:nth-of-type(even) .plaque { --tilt: 0deg; }
  /* the wedge points up at the node */
  .plaque::after {
    top: -12px;
    left: 50%;
    margin-left: -12px;
    width: 24px;
    height: 12px;
    clip-path: polygon(50% 0, 0 100%, 100% 100%);
  }
  /* The milestone sits on the route: its badge is 36px off the centre line,
     left on odd rows and right on even ones, and the label runs away from
     the line and wraps rather than leave the paper. The route bends out to
     meet the badge and back for the next node. */
  .mile {
    grid-column: 1;
    grid-row: 3;
    left: 0;
    margin: 16px 0 0;
    max-width: calc(50% + 51px);
    white-space: normal;
    line-height: 1.3;
    text-align: left;
  }
  .ch:nth-of-type(odd) .mile { justify-self: start; margin-left: calc(50% - 55px); }
  .ch:nth-of-type(even) .mile {
    justify-self: end;
    margin-right: calc(50% - 55px);
    flex-direction: row-reverse;
    padding: 3px 3px 3px 13px;
    text-align: right;
  }
  .badge { width: 28px; height: 28px; }
  .badge svg { width: 15px; height: 15px; }
  .notes { grid-template-columns: 1fr; gap: 18px; margin-top: 22px; }
  .note { padding: 16px 16px 14px; --r: 0deg; }
  .close { padding: 6px 4px 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: var(--rim); }
}

/* ---------- 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, body::after { 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; }
