/* 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
 * meanders, numbered nodes, a pennant on every chapter that is finished, tilted
 * plaques with a thick edge, papercraft offsets and a paper grain.
 *
 * The palette is Forgejo's own, read out of its theme CSS.
 *
 * 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 -------------------------------------------------------- */

:root {
  color-scheme: dark;

  /* Forgejo's steel ramp, read out of theme-forgejo-dark.css. Dark is the
     default because that is what Forgejo ships looking like. */
  --field: #171e26;        /* steel-800 */
  --tile: #242d38;         /* steel-700 */
  --tile-hi: #2b3642;      /* steel-650 */
  --ink: #d2e0f0;          /* steel-100 */
  --ink-2: #8c9caf;        /* steel-300 */
  --rule: #374351;         /* steel-600 */

  --paper: #131a21;        /* steel-850, the map sits below the chrome */
  --plaque: #1d262f;       /* steel-750 */
  --plaque-ink: #d2e0f0;
  --plaque-ink-2: #aebed0; /* steel-200 */
  --edge: #5f6e80;         /* steel-450 */
  --drop: #10161d;         /* steel-900, the papercraft offset */

  /* five accents: Forgejo's orange primary walked down into a pastel red */
  --a1: #fed7aa;
  --a2: #fdba74;
  --a3: #fb923c;
  --a4: #fca5a5;
  --a5: #f87171;

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

  /* 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, so it does not turn
     dark orange when the page turns light. */
  --note-a: #fed7aa;
  --note-b: #fdba74;
  --note-c: #fb923c;
  --note-d: #fca5a5;
  --note-ink: #171e26;

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

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

/* Forgejo light. The accents deepen with it: a pastel that carries 8:1 on steel
   carries 2:1 on zinc, so the ramp resolves per theme rather than per use. */
[data-theme="light"] {
  color-scheme: light;
  --field: #f4f4f5;
  --tile: #ffffff;
  --tile-hi: #ffffff;
  --ink: #18181b;
  --ink-2: #52525b;
  --rule: #e4e4e7;
  --paper: #fafafa;
  --plaque: #ffffff;
  --plaque-ink: #18181b;
  --plaque-ink-2: #3f3f46;
  --edge: #a1a1aa;
  --drop: #d4d4d8;
  --a1: #ea580c;
  --a2: #c2410c;
  --a3: #9a3412;
  --a4: #dc2626;
  --a5: #b91c1c;
  --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);
  --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");
}

/* 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(--a3);
  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: flex;
  align-items: center;
  justify-content: space-between;
  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 {
  display: flex;
  align-items: center;
  gap: 16px;
}

.clock {
  font-family: var(--f-mono);
  font-size: 0.8125rem;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* 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(--field);
}
.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(--a3); 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. */
.plate {
  display: none;
  min-height: 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(--field);
  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(--field);
  /* 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(--field);
  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(--field) 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(--field);
}
.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(--field);
  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 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(--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 ----------------------------------------------------- */

/* Beads on a gentle wave. Six gradient layers, one bead each per period at
   its own x and y, tile into a route that sways as it goes: equal beads, equal
   spacing, six samples per period so the curve reads as a curve and not as a
   zigzag. Each bead is lit from the centre so it reads as a bead rather than
   a dot. It stretches with the content, which SVG coordinates would not. */
.ch::before,
.title::before {
  content: "";
  position: absolute;
  left: 23px;
  top: 50px;
  bottom: 0;
  width: 28px;
  transform: translateX(-50%);
  --bead: var(--a1) 0, var(--a3) 1.7px, var(--a3) 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);
}

/* ---------- 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. It sways a
   little on its own and flutters when the chapter is picked. */
.flag {
  position: absolute;
  right: 100%;
  top: -21px;
  margin-right: 3px;
  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;
}
.flag {
  color: var(--hue);
  --fsh: var(--drop);
  --fpole: var(--edge);
}
@keyframes wave {
  0%, 100% { rotate: 0deg; }
  50%      { rotate: -4deg; }
}
.ch:hover .flag,
.ch:focus-within .flag { animation: 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(--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(--field);
  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. */
.js .rv .plaque,
.js .rv .title-card,
.js .rv.close > * { opacity: 0; }
.js .rv .flag { scale: 0; }
.pop .plaque { animation: plaque-in 0.6s var(--pop) both; }
.pop .title-card { animation: card-in 0.6s var(--pop) both; }
.pop.close > * { animation: card-in 0.55s var(--pop) both; }
.pop .flag {
  animation:
    plant 0.5s var(--pop) 0.28s both,
    wave 3.6s ease-in-out 0.8s infinite;
}
.pop .node { animation: node-in 0.5s var(--pop) both; }
@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; }

/* ---------- 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;
  background:
    linear-gradient(180deg, rgb(0 0 0 / 0.07) 0 17px, transparent 17px),
    var(--paper-note);
  color: var(--note-ink);
  border-radius: 2px;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.25),
    4px 5px 0 var(--drop),
    0 10px 18px rgb(0 0 0 / 0.22);
  --r: 0deg;
  transform: rotate(var(--r));
  transition: transform var(--t) var(--pop), box-shadow 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; }
/* the curled corner: the plaque shows through the cut, the fold is darker paper */
.note::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  border-style: solid;
  border-width: 0 0 22px 22px;
  border-color: transparent transparent var(--plaque) rgb(0 0 0 / 0.16);
  border-bottom-left-radius: 2px;
}
.note:hover {
  transform: rotate(0deg) translateY(-4px) scale(1.02);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.25),
    7px 9px 0 var(--drop),
    0 18px 26px 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 and the wedge swap
     with it */
  .ch:nth-of-type(even) .flag {
    right: auto;
    left: 100%;
    margin: 0 0 0 3px;
    transform: none;
  }
  .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; }
  .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; }
  /* 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(--a1) 0, var(--a3) 1.5px, var(--a3) 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; }
  .flag { width: 25px; height: 28px; top: -18px; margin-right: 2px; }
  .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; }
  .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.close > * { opacity: 1; }
  .js .rv .flag { 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 { box-shadow: none; transform: none; }
  .js .rv .plaque, .js .rv .title-card, .js .rv.close > * { opacity: 1; }
  .js .rv .flag { 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; }
