:root {
  --ink: #0a0a0b;
  --soft: rgb(0 0 0 / 0.84);
  --muted: rgb(0 0 0 / 0.68);
  --pad: clamp(20px, 3.4vw, 56px);
  --r: clamp(26px, 3vw, 44px);               /* card corners */
  --r-in: clamp(16px, 1.8vw, 26px);          /* corners of things inside a card */
  --sans: "Helvetica Neue", Helvetica, "Nimbus Sans", Arimo, Arial, sans-serif;
  --spring: cubic-bezier(0.3, 1.35, 0.4, 1);  /* overshoots a little, then settles */
}
/* A real spring curve where the browser can draw one */
@supports (transition-timing-function: linear(0, 1)) {
  :root { --spring: linear(0, 0.042, 0.145, 0.281, 0.428, 0.571, 0.7, 0.81, 0.898, 0.964, 1.012, 1.042, 1.059, 1.066, 1.065, 1.06, 1.051, 1.042, 1.032, 1.023, 1.015, 1.009, 1.004, 1, 0.998, 0.996, 0.996, 0.996, 0.996, 0.996, 1); }
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }
html, body { height: 100%; }

body {
  margin: 0;
  overflow: hidden;
  background: #eceae6;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.35;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p, ul, figure { margin: 0; padding: 0; }
ul { list-style: none; }
a { color: inherit; text-decoration: none; }
img, video { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }
em { font-style: italic; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---------- Ambient colour behind the glass ---------- */
.ambient { position: fixed; inset: 0; z-index: 0; overflow: hidden; background: #eceae6; }
.ambient-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.1s ease;
  background: var(--tone);
}
/* Each look's colour, picked from its card's artwork: the layers behind the glass take it, and so
   do the cards themselves, which on a phone carry their own (see the phones' styles) */
[data-ambient="night"] { --tone: #132a40; }
[data-ambient="mix"] { --tone: #f3dde3; }
[data-ambient="p4"] { --tone: #5e273b; }
[data-ambient="love"] { --tone: #4c1306; }
[data-ambient="popstar"] { --tone: #002975; }
[data-ambient="blank101"] { --tone: #a40606; }
[data-ambient="photo"] { --tone: #151615; }
[data-ambient="film"] { --tone: #706a53; }
.ambient-layer.is-on { opacity: 1; }
/* With the script running, it sets each layer's opacity as the page scrolls */
.js .ambient-layer { transition: none; }
/* The glass tint: with the script running it is one fixed layer, clear over light cards and
   smoked over dark ones, blended between them as the page scrolls, so no card has an edge.
   Without the script each card carries its own tint, as below. */
.veil { position: absolute; inset: 0; }
.veil-light { background: rgb(255 255 255 / 0.33); }
.veil-dark { background: rgb(9 7 11 / 0.69); opacity: 0; }
html:not(.js) .veil { display: none; }

/* ---------- Glass ---------- */
.glass, .glass-clear { position: relative; }
/* Card glass: a see-through tint over the colour behind it. That colour is one solid colour,
   so the cards need no live blur of their own, which is what keeps scrolling light.
   Light cards lift what is behind them and carry dark text; cards over dark artwork
   (data-tone="dark") are smoked glass that darkens what is behind them and carry light text. */
.glass {
  color: var(--ink);
  background: rgb(255 255 255 / 0.33);
  box-shadow:
    0 30px 70px -24px rgb(0 0 0 / 0.4),
    0 2px 6px rgb(0 0 0 / 0.06);
}
.slide[data-tone="dark"] .glass,
body[data-tone="dark"] .sidebar {
  --ink: #fff;
  --soft: rgb(255 255 255 / 0.88);
  --muted: rgb(255 255 255 / 0.7);
  background: rgb(9 7 11 / 0.69);
}
/* With the script running, the tint is on the fixed veil behind the cards instead (see above) */
.js .slide .card.glass { background: none; }
/* Clear glass for controls. The tint is set by variables so the floating controls
   can turn into dark glass with light text while a dark card is on show. */
.glass-clear {
  --g-tint: rgb(255 255 255 / 0.3);
  --g-ink: var(--ink);
  --g-muted: var(--muted);
  color: var(--g-ink);
  background-color: var(--g-tint);
  box-shadow:
    inset 0 1px 1px rgb(255 255 255 / 0.75),
    inset 0 -8px 14px -10px rgb(255 255 255 / 0.65),
    0 10px 26px -12px rgb(0 0 0 / 0.42),
    0 1px 2px rgb(0 0 0 / 0.08);
  transition: color 0.6s ease, background-color 0.6s ease;
}
/* Only the small floating controls blur what is behind them: cards really do slide under those */
.controls .glass-clear {
  -webkit-backdrop-filter: blur(18px) saturate(200%) brightness(1.06);
  backdrop-filter: blur(18px) saturate(200%) brightness(1.06);
}
body[data-tone="dark"] .controls .glass-clear {
  --g-tint: rgb(16 16 20 / 0.36);
  --g-ink: #fff;
  --g-muted: rgb(255 255 255 / 0.7);
}
/* Glass sitting on a card needs a hairline edge to stand out from the glass under it */
.card .glass-clear {
  --g-tint: rgb(255 255 255 / 0.5);
  box-shadow:
    inset 0 1px 1px rgb(255 255 255 / 0.9),
    inset 0 -8px 14px -10px rgb(255 255 255 / 0.7),
    0 0 0 1px rgb(0 0 0 / 0.11),
    0 10px 24px -12px rgb(0 0 0 / 0.42);
}
/* The bright rim that catches the light along the edge: brightest top-left and bottom-right */
.glass::before, .glass-clear::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  box-shadow:
    inset 1px 1px 0 rgb(255 255 255 / 0.8),
    inset -1px -1px 0 rgb(255 255 255 / 0.5),
    inset 0 0 0 1px rgb(255 255 255 / 0.16);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .controls .glass-clear { --g-tint: rgb(255 255 255 / 0.88); }
  body[data-tone="dark"] .controls .glass-clear { --g-tint: rgb(24 24 28 / 0.88); }
}

.pill {
  display: inline-flex;
  align-items: center;
  height: 40px;
  padding: 0 16px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.round {
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.round:hover { transform: scale(1.06); }
.round:active { transform: scale(0.94); }
.round:disabled { opacity: 0.4; cursor: default; transform: none; }
.round svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- Floating controls ---------- */
.controls {
  position: fixed;
  z-index: 5;
  /* tucked into the bottom right corner of the card */
  right: clamp(10px, 1.4vw, 20px);
  bottom: clamp(10px, 1.4vw, 16px);
  display: flex;
  align-items: center;
  gap: 8px;
}
.counter { gap: 0.4em; font-weight: 400; color: var(--g-muted); font-variant-numeric: tabular-nums; }
.counter em { font-weight: 700; color: var(--g-ink); }
/* The way back to the cover, where the contents are. It steps aside while the cover is on show. */
.to-index { transition: color 0.6s ease, background-color 0.6s ease, opacity 0.4s ease, scale 0.4s var(--spring); }
.to-index:hover { scale: 1.05; }
/* hidden until the script says the cover has been left, so it never flashes on load */
.js .to-index { opacity: 0; pointer-events: none; }
.js .past-cover .to-index { opacity: 1; pointer-events: auto; }

/* ---------- The sidebar: the contents again, down the left ----------
   The same scattered words as the cover, in a tall glass bar. On wide screens it stands beside
   every card but the cover (the cover is itself the contents, so it keeps the full width);
   on narrow screens it is a drawer that the "index" pill opens. It takes its tone, light or
   smoked, from the card on show, and the word for the current chapter is straightened and inked. */
:root { --side-w: 0px; }
.sidebar {
  position: fixed;
  z-index: 4;
  container-type: size;
  border-radius: var(--r);
  background: rgb(255 255 255 / 0.34);
}
body[data-tone="dark"] .sidebar { background: rgb(9 7 11 / 0.7); }
.side-home {
  position: absolute;
  left: 10.5cqw;
  top: clamp(26px, 6.2cqh, 50px);
  font-size: clamp(18px, 15.5cqw, 34px);
  font-weight: 700;
  letter-spacing: -0.055em;
  line-height: 0.9;
}
.side-home em {
  display: block;
  margin-top: 0.12em;
  font-size: 0.5em;
  font-weight: 300;
  letter-spacing: -0.04em;
  text-align: right;
}
.side-nav { position: absolute; inset: 0; translate: -6cqw 0; pointer-events: none; }   /* a little to the left, clear of the card's rule */
.side-link {
  pointer-events: auto;
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  translate: -50% -50%;
  padding: 9px 10px;
  font-size: calc(clamp(16px, 13cqw, 30px) * var(--s, 1));
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1;
  white-space: nowrap;
  color: var(--muted);
  transition: color 0.4s ease, left 0.6s var(--spring);
}
.side-link span {
  display: inline-block;
  rotate: var(--r, 0deg);
  transition: rotate 0.6s var(--spring), scale 0.6s var(--spring);
}
.side-link:hover, .side-link:focus-visible, .side-link.active { color: var(--ink); }
.side-link:hover span, .side-link:focus-visible span { rotate: 0deg; scale: 1.1; }
.side-link:active span { scale: 0.96; }
/* The current chapter moves to the middle of the bar to have room to grow */
.side-link.active { left: 50%; z-index: 2; }

/* ---------- The picked chapter word, set like a Persona 5 menu ----------
   The script splits each chapter word into letters (`rl`) and gives every letter its own
   font, case, tilt, size and look: a black box, a white box, a red box, or bare outlined ink.
   They only show when the word is picked: the current chapter in the sidebar, or a cover
   link the moment it is clicked. The word then grows (--grow is worked out so it fits the
   bar) on a jagged red shape. */
.rl { display: inline-block; font-style: inherit; }
.side-link.active span,
.index-link.picked span {
  position: relative;
  isolation: isolate;
  rotate: -4deg;
  scale: var(--grow, 1.4);
  letter-spacing: 0;
}
.side-link.active span::before,
.index-link.picked span::before {
  content: "";
  position: absolute;
  inset: -0.32em -0.42em -0.26em -0.36em;
  z-index: -1;
  background: #e5191e;
  clip-path: polygon(4% 20%, 28% 2%, 61% 14%, 99% 0, 93% 50%, 100% 97%, 60% 86%, 24% 100%, 0 80%, 7% 47%);
}
.side-link.active .rl,
.index-link.picked .rl {
  margin: 0 0.015em;
  padding: 0.05em 0.09em 0.03em;
  font-family: var(--rf);
  font-style: normal;
  font-weight: 700;
  line-height: 1;
  text-transform: var(--rc);
  rotate: var(--rr);
  scale: var(--rs);
  translate: 0 var(--ry);
}
.side-link.active .rl-black, .index-link.picked .rl-black { background: #0b0b0b; color: #fff; box-shadow: 0 0 0 0.05em #fff; }
.side-link.active .rl-white, .index-link.picked .rl-white { background: #fff; color: #0b0b0b; box-shadow: 0 0 0 0.05em #0b0b0b; }
.side-link.active .rl-red, .index-link.picked .rl-red { background: #e5191e; color: #fff; box-shadow: 0 0 0 0.05em #0b0b0b; }
.side-link.active .rl-ink, .index-link.picked .rl-ink {
  color: #fff;
  -webkit-text-stroke: 0.14em #0b0b0b;
  paint-order: stroke fill;
}
@media (prefers-reduced-motion: no-preference) {
  /* The red slash slams open sideways past its size and snaps back; each letter pops up in
     its place from nothing, twisting, swells well past its size, then settles. Fast and hard,
     with the letters close on each other's heels */
  .side-link.active span::before, .index-link.picked span::before { animation: ransom-slash 0.36s cubic-bezier(0.2, 0.9, 0.3, 1) backwards; }
  .side-link.active .rl, .index-link.picked .rl {
    animation: ransom-pop 0.46s cubic-bezier(0.2, 0.9, 0.3, 1) backwards;
    animation-delay: calc(var(--i) * 18ms);
  }
}
@keyframes ransom-slash {
  0% { opacity: 0; scale: 0 1; }
  45% { opacity: 1; scale: 1.22 1.1; }
  72% { scale: 0.96 1; }
  100% { scale: 1; }
}
@keyframes ransom-pop {
  0% { opacity: 0; scale: 0; rotate: calc(var(--rr) * -6); }
  42% { opacity: 1; scale: calc(var(--rs) * 1.55); rotate: calc(var(--rr) * 1.8); }
  68% { scale: calc(var(--rs) * 0.9); }
}
.side-scrim { display: none; }

/* Wide screens: the bar stands to the left of the cards */
@media (min-width: 1000px) {
  :root { --side-w: clamp(150px, 13vw, 200px); }
  /* No panel of its own: it floats over the left column of whichever card is on show */
  .sidebar {
    left: calc(var(--pad) * 0.5);
    top: 0;
    width: var(--side-w);
    height: 100vh;
    height: 100svh;
    background: none;
    box-shadow: none;
  }
  body[data-tone="dark"] .sidebar { background: none; }
  .sidebar::before { display: none; }
  html:not(.js) .sidebar { display: none; }
  /* It steps in once the cover has been left, and out again on the way back */
  .js .sidebar {
    opacity: 0;
    translate: -28px 0;
    visibility: hidden;
    transition: opacity 0.45s ease, translate 0.7s var(--spring), visibility 0s linear 0.45s, background-color 0.6s ease, color 0.6s ease;
  }
  .js .past-cover .sidebar {
    opacity: 1;
    translate: 0 0;
    visibility: visible;
    transition: opacity 0.45s ease 0.12s, translate 0.7s var(--spring) 0.12s, visibility 0s linear, background-color 0.6s ease, color 0.6s ease;
  }
  .to-index { display: none; }
}

/* Narrow screens: the bar is a drawer */
@media (max-width: 999.98px) {
  .sidebar {
    left: 10px;
    top: 10px;
    bottom: 10px;
    width: min(72vw, 264px);
    z-index: 40;
    background: rgb(255 255 255 / 0.8);
    translate: calc(-100% - 24px) 0;
    visibility: hidden;
    transition: translate 0.6s var(--spring), visibility 0s linear 0.6s, background-color 0.6s ease, color 0.6s ease;
  }
  body[data-tone="dark"] .sidebar { background: rgb(14 12 16 / 0.86); }
  .sidebar.is-open {
    translate: 0 0;
    visibility: visible;
    transition-delay: 0s;
    -webkit-backdrop-filter: blur(22px) saturate(170%);
    backdrop-filter: blur(22px) saturate(170%);
  }
  .side-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 39;
    background: rgb(0 0 0 / 0.3);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.35s ease;
  }
  .sidebar.is-open + .side-scrim { opacity: 1; pointer-events: auto; }
}

/* The page rail: one mark per card, spaced into chapters. The script builds it from the
   cards and sets it between the two arrows. */
.rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  color: var(--ink);
  transition: color 0.6s ease;
}
body[data-tone="dark"] .rail { color: #fff; }
.rail-dot {
  position: relative;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 99px;
  background: currentColor;
  opacity: 0.32;
  transition: height 0.6s var(--spring), opacity 0.3s ease;
}
.rail-dot.starts-chapter { margin-top: 9px; }
.rail-dot:hover, .rail-dot:focus-visible { opacity: 0.8; }
.rail-dot.is-on { height: 26px; opacity: 1; }
/* a roomier target than the mark itself */
.rail-dot::before { content: ""; position: absolute; inset: -4px -12px; }
.rail-dot::after {
  content: attr(data-label);
  position: absolute;
  right: 20px;
  top: 50%;
  translate: 8px -50%;
  padding: 6px 11px;
  border-radius: 99px;
  background: rgb(18 18 22 / 0.88);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, translate 0.4s var(--spring);
}
.rail-dot:hover::after, .rail-dot:focus-visible::after { opacity: 1; translate: 0 -50%; }

/* Wide screens: no page strip. The sidebar, scrolling and the arrow keys do the moving.
   (Delete this rule to bring the strip back: arrows, page rail and counter inside the
   card's right edge, laid out by the rule below.) */
@media (min-width: 1000px) {
  .controls { display: none; }
}
@media (min-width: 1000px) {
  .controls {
    right: calc((var(--pad) - 28px) / 2);
    top: 50%;
    bottom: auto;
    translate: 0 -50%;
    flex-direction: column;
    gap: 12px;
    width: 28px;
  }
  .controls [data-go="prev"] { order: 1; }
  .controls .rail { order: 2; }
  .controls [data-go="next"] { order: 3; }
  .controls .counter { order: 4; }
  .controls .round { width: 28px; height: 28px; }
  .controls .round svg { width: 13px; height: 13px; }
  .controls .counter {
    flex-direction: column;
    gap: 4px;
    height: auto;
    padding: 2px 0 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    font-size: 11px;
    line-height: 1;
  }
  .controls .counter::before { display: none; }
  .counter-sep { width: 12px; height: 1px; font-size: 0; background: currentColor; opacity: 0.6; }
}
@media (min-width: 1000px) and (max-height: 540px) {
  .controls .round { display: none; }
}
/* Narrow screens: just the index pill and the counter, floating in the card's bottom corner,
   with room kept under the card's content so its last lines can scroll clear of them */
@media (max-width: 999.98px) {
  .controls .round, .rail { display: none; }
  .card { --foot: 64px; }
  /* the cover keeps nothing at its foot, so its title is centred on the whole screen */
  .slide#top .card .card-body { padding-bottom: var(--pad); }
}

.slide[data-tone="dark"] .card .glass-clear {
  --g-tint: rgb(255 255 255 / 0.14);
  box-shadow:
    inset 0 1px 1px rgb(255 255 255 / 0.5),
    inset 0 -8px 14px -10px rgb(255 255 255 / 0.3),
    0 0 0 1px rgb(255 255 255 / 0.12),
    0 10px 24px -12px rgb(0 0 0 / 0.6);
}

/* ---------- The carousel ---------- */
.deck {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.deck::-webkit-scrollbar { display: none; }
/* Touch screens: the page itself scrolls, not the deck, so a phone scrolls it exactly as it scrolls
   any page: its address bar tucks away, a tap at the top of the screen goes back up, and it bounces
   at the ends as it should. (html.page-scroll is set in index.html, before anything is drawn.) The
   colour behind the glass is as tall as the screen with its address bar tucked away, so it never
   jumps as the bar comes and goes. */
.page-scroll body { height: auto; overflow: visible; filter: none; }
.page-scroll .deck { position: relative; inset: auto; overflow: visible; overscroll-behavior: auto; }
.page-scroll .ambient { bottom: auto; height: 100vh; height: 100lvh; }

.slide {
  flex: none;
  width: 100%;
  height: 100vh;
  height: 100svh;
}
/* The road (road.js): on a wide screen the cards stand along a path into the screen. The view stays
   put while the deck scrolls past a stop for each card beneath it (it takes no room of its own), and
   a card that is not the one in front is a smaller pane of glass further along, which the pointer
   passes through */
.road .deck { display: block; }
.road-view {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  margin-bottom: -100vh;
  margin-bottom: -100svh;
  overflow: hidden;
}
.road-path { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.road-cards { position: absolute; inset: 0; }
/* each card's stretch of scrolling: a screen and a half, so a swipe on a trackpad, momentum and all,
   carries the ride about two thirds as far as a screen a card would, and a gentle flick takes it to
   the next card rather than past it (a wheel's notch still moves one card) */
.road-stop { height: 150vh; height: 150svh; }
/* the pane's glass: a layer of its own behind the card, faded only by its opacity (--panel), so the
   flight never makes the browser paint a card again */
.road .slide { --panel: 0; }
.road .slide::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 36px;
  background: rgb(255 255 255 / 0.24);
  box-shadow: 0 50px 110px -40px rgb(0 0 0 / 0.55);
  opacity: var(--panel);
  pointer-events: none;
}
.road .slide[data-tone="dark"]::before { background: rgb(12 10 14 / 0.55); }
.road .slide.road-far { pointer-events: none !important; }

/* One big card: on wide screens every card but the cover keeps its left column free for
   the sidebar, which floats there while the cards move beneath it */
.js .slide:not(#top) .card { padding-left: var(--side-w); }
/* The grid: each card after the cover is ruled into cells, Swiss style, the rules running to the
   card's edges. --rule is the ink of the card (black on clear glass, white on smoked), --cell the
   room inside each cell, and --foot the room kept at the foot of the bottom cells for the floating
   controls on a narrow screen. The rules lie on the same glass as the words, so they bend with
   the CRT's curve like everything else. */
.card {
  --rule-w: 2px;
  --rule: color-mix(in srgb, var(--ink) 90%, transparent);
  --cell: clamp(16px, min(2.2cqw, 4.4cqh), 40px);
  --foot: 0px;
}
.slide:not(#top) .card-body { padding: 0; }
/* Where the cards stand one under another down the page (a phone or tablet, motion turned down,
   no script), a rule runs across the top of each, so the column rules of one card and the next
   meet a line instead of stopping in the air. It starts at the sidebar's rule, leaving the sidebar
   clear as ever. On the road each card is the whole screen alone. */
.slide:not(#top) .card-body { border-top: var(--rule-w) solid var(--rule); }
@media (min-width: 1000px) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .js .slide:not(#top) .card-body { border-top: 0; }
}
/* on a wide screen, one rule down the whole card where the sidebar ends */
@media (min-width: 1000px) {
  .js .slide:not(#top) .card-body { border-left: var(--rule-w) solid var(--rule); }
}

/* Every card fills the screen, edge to edge, with no frame. The script dims each card by how
   far it is from the centre. */
.card {
  height: 100%;
  overflow: hidden;
  container-type: size;
  box-shadow: none;
}
/* On a card whose content scrolls inside it, the content is blurred as it runs into the top
   or bottom of the screen */
.card.glass::before, .card::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  z-index: 3;
  height: clamp(50px, 7vw, 108px);
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.card.glass::before { top: 0; inset-block-end: auto; -webkit-mask-image: linear-gradient(#000 35%, transparent); mask-image: linear-gradient(#000 35%, transparent); }
.card::after { bottom: 0; -webkit-mask-image: linear-gradient(transparent, #000 65%); mask-image: linear-gradient(transparent, #000 65%); }
.slide.is-current.scrolls .card.glass::before, .slide.is-current.scrolls .card::after {
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.slide.is-current.scrolled .card.glass::before, .slide.is-current.more-below .card::after { opacity: 1; }
.card-body {
  height: 100%;
  padding: var(--pad);
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgb(0 0 0 / 0.2) transparent;
}

/* ---------- Shared pieces ---------- */
.frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-in);
  background: rgb(0 0 0 / 0.08);
  aspect-ratio: var(--ratio, 4 / 3);
  box-shadow: 0 16px 40px -18px rgb(0 0 0 / 0.45);
}
.frame img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

.label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: 12px;
}

.statement {
  font-size: clamp(22px, 3vw, 52px);
  font-size: clamp(22px, min(3.25cqw, 6.6cqh), 58px);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1.04;
  max-width: 13.5em;
  text-wrap: balance;
}
/* Light italic against the bold upright */
.statement em, .lockup-year em, .closing-mark em { font-weight: 300; letter-spacing: -0.04em; }

/* ---------- Cover ---------- */
.cover { position: relative; display: grid; place-items: center; }
.cover, .project, .closing { height: 100%; }
.about { min-height: 100%; }
.lockup { position: relative; display: inline-block; }   /* above the chant's canvas */
/* The title is centred on the cover by itself, so the chanted words land in the middle of the
   screen: "(mixed media)" hangs just below it and takes no part in the centring */
.lockup-foot { position: absolute; top: 100%; right: 0; white-space: nowrap; }
/* The chanted title: the script draws the words on a canvas across the cover, with a flash
   beneath for the bold words, and the heading's own ink is hidden. Without the script's canvas
   (or with reduced motion) the heading shows as it is. */
.cover > .chant, .cover > .chant-flash { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.chant-flash { background: rgb(196 222 240); opacity: 0; }
.lockup h1 {
  font-size: clamp(56px, 17vw, 300px);
  font-size: clamp(56px, min(18cqw, 42cqh), 340px);
  font-weight: 700;
  letter-spacing: -0.055em;
  line-height: 0.86;
}
.lockup-foot { display: flex; justify-content: flex-end; margin-top: 0.85em; }
/* While the title chants, its line "(mixed media)" shows only under PORTFOLIO., once the word has
   landed (script.js): the other words, and the slams, never crowd it. It settles in like a subtitle
   coming into focus: rising a little into place out of a blur, slowing as it arrives. It leaves with
   a quick soft fade and no movement, ahead of the hard cut, and drops back below, unseen, ready to
   rise again next time. */
.js .cover.chanting .lockup-foot {
  opacity: 0;
  transform: translateY(0.4em);
  filter: blur(7px);
  transition: opacity 0.22s ease-in, filter 0.22s ease-in, transform 0s linear 0.24s;
}
.js .cover.chanting .lockup-foot.is-on {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition: opacity 0.9s cubic-bezier(0.25, 0.8, 0.35, 1), filter 1s cubic-bezier(0.25, 0.8, 0.35, 1), transform 1.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.lockup-year { font-size: clamp(20px, 3.8cqw, 68px); line-height: 1; }
/* The contents: chapter names scattered over the cover as plain text.
   Each link is placed by its centre (--x/--y, or --mx/--my on phones, in percent of the card),
   tilted by --r and sized by --s. The inner span does the tilting, so it can straighten on hover. */
.index { position: absolute; inset: 0; pointer-events: none; }
.index-link {
  pointer-events: auto;
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  translate: -50% -50%;
  padding: 10px 12px;
  font-size: clamp(21px, 2.5vw, 38px);
  font-size: calc(clamp(21px, min(2.5cqw, 5.6cqh), 40px) * var(--s, 1));
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1;
  white-space: nowrap;
}
.index-link span {
  display: inline-block;
  rotate: var(--r, 0deg);
  transition: rotate 0.6s var(--spring), scale 0.6s var(--spring);
}
.index-link em, .side-link em { font-weight: 300; letter-spacing: -0.04em; }
.index-link:not(.picked):hover span, .index-link:not(.picked):focus-visible span {
  rotate: 0deg;
  scale: 1.1;
  text-decoration: underline;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.14em;
}
.index-link:active span { scale: 0.96; }

/* The black and white switch, in the cover's top right corner */
.mono-toggle {
  position: absolute;
  top: clamp(12px, 3cqh, 30px);
  right: clamp(14px, 2.6cqw, 44px);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 4px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: clamp(13px, 1.15cqw, 16px);
  cursor: pointer;
  transition: color 0.3s ease;
}
.mono-toggle:hover, .mono-toggle[aria-pressed="true"] { color: var(--ink); }
.mono-switch {
  position: relative;
  flex: none;
  width: 36px;
  height: 20px;
  border-radius: 99px;
  box-shadow: inset 0 0 0 1.5px currentColor;
}
/* the knob is half white and half black, and slides over when the switch is on */
.mono-switch span {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: linear-gradient(90deg, currentColor 50%, transparent 50%);
  box-shadow: inset 0 0 0 1.5px currentColor;
  transition: translate 0.45s var(--spring), rotate 0.45s var(--spring);
}
.mono-toggle[aria-pressed="true"] .mono-switch span { translate: 16px 0; rotate: 180deg; }

/* ---------- About ---------- */
/* One grid of cells: the statement over the bio on the left, the CV's four groups in two columns
   (education over experience, recognition over community, each pair split at its own height), and
   the tools along the foot. The wrappers give their pieces to the grid. */
.about {
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.5fr) minmax(0, 0.5fr);
  grid-template-rows: auto auto minmax(min-content, 1fr) auto;   /* never shorter than its words: a short card scrolls */
  grid-template-areas:
    "statement edu  rec"
    "statement exp  rec"
    "bio       exp  comm"
    "tools     frames frames";
}
.about-main, .about .side, .about .cv, .about .cv-col { display: contents; }
.about .statement { grid-area: statement; }
/* a statement in a cell keeps its measure inside the cell's room */
.about .statement, .closing-main > .statement { max-width: calc(13.5em + var(--cell) * 2); }
.about .about-row { grid-area: bio; align-self: stretch; align-content: center; border-top: var(--rule-w) solid var(--rule); }
.about .toolkit { grid-area: tools; border-top: var(--rule-w) solid var(--rule); }
.about .frameworks { grid-area: frames; border-top: var(--rule-w) solid var(--rule); }
.about .cv-col > div { border-left: var(--rule-w) solid var(--rule); }
.about .cv-col:first-child > :first-child { grid-area: edu; }
.about .cv-col:first-child > :last-child { grid-area: exp; border-top: var(--rule-w) solid var(--rule); }
.about .cv-col:last-child > :first-child { grid-area: rec; }
.about .cv-col:last-child > :last-child { grid-area: comm; border-top: var(--rule-w) solid var(--rule); }
.about .statement, .about .about-row, .about .toolkit, .about .frameworks, .about .cv-col > div { min-width: 0; padding: var(--cell); }
.about .toolkit, .about .frameworks { padding-bottom: calc(var(--cell) + var(--foot)); }
.about-row {
  display: grid;
  grid-template-columns: minmax(96px, min(30%, 27cqh)) minmax(0, 30em);
  gap: clamp(14px, 2cqw, 28px);
  align-items: start;
}
.bio { color: var(--soft); }
.bio strong { color: var(--ink); }
.bio p + p { margin-top: 0.9em; }
.bio a { text-decoration: underline; text-underline-offset: 2px; }
/* a site's own icon in front of its name, the size of the letters, never left at a line's end */
.site-link { white-space: nowrap; }
.site-icon { display: inline-block; width: 1.15em; height: 1.15em; margin-right: 0.28em; vertical-align: -0.22em; }

.cv {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(20px, 3cqw, 48px);
  align-content: start;
}
.cv-col { display: grid; gap: clamp(22px, 5cqh, 44px); align-content: start; }
.cv li { display: grid; grid-template-columns: 5.6em minmax(0, 1fr); gap: 10px; }
.cv li + li { margin-top: 16px; }
.cv .when { font-size: 9px; text-transform: uppercase; letter-spacing: 0.02em; color: var(--muted); padding-top: 3px; }
.cv small { font-size: 11px; font-style: italic; color: var(--muted); }
.cv a { text-decoration: underline; text-underline-offset: 2px; }

.side { display: flex; flex-direction: column; justify-content: space-between; gap: clamp(22px, 5cqh, 44px); min-width: 0; }
/* the software and the languages, side by side, each group under its own label. Ten tiles share
   the bio's column, so on a wide card they are sized to its width (about two fifths of the card) */
.toolkit { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 14px clamp(16px, 1.8cqw, 48px); }
.toolkit .tool-list, .frameworks .tool-list { gap: 12px clamp(9px, 0.85cqw, 24px); }
.toolkit .app, .frameworks .app { width: clamp(34px, min(8cqh, 3.3cqw), 60px); }
/* every name is given room for two lines, so the three groups' labels line up across the card */
.toolkit .app-name, .frameworks .app-name { font-size: 10px; min-height: 2.4em; }
.tool-list { display: flex; flex-wrap: wrap; gap: 14px clamp(14px, 2.4cqw, 30px); }
.tool-list li { display: grid; gap: 8px; justify-items: start; width: min-content; }
/* App-icon tiles */
.app {
  display: block;
  width: clamp(46px, 8cqh, 60px);
  aspect-ratio: 1;
  border-radius: 23%;
  overflow: hidden;
  background: var(--tile, #fff);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.4),
    inset 0 0 0 1px rgb(255 255 255 / 0.14),
    0 12px 24px -12px rgb(0 0 0 / 0.6);
}
.app svg { display: block; width: 100%; height: 100%; }
.app-name { font-size: 11px; line-height: 1.2; color: var(--soft); }

/* ---------- Projects: text beside a stage that swaps between the pieces ---------- */
.project {
  display: grid;
  grid-template-columns: minmax(230px, 31%) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}
/* The text column: the card's number in a cell at the top, the words at the foot, and the picker
   ruled off beneath them, reaching across the column */
.project-text {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-width: 0;
  border-right: var(--rule-w) solid var(--rule);
}
.project-no {
  padding: var(--cell);
  border-bottom: var(--rule-w) solid var(--rule);
  font-size: clamp(30px, min(4.4cqw, 9cqh), 84px);
  font-weight: 300;
  letter-spacing: -0.06em;
  line-height: 0.8;
  font-variant-numeric: tabular-nums;
}
/* Between the number and the words, a cell with a circle in it, as big as the cell allows, touching
   its rules, and a diameter that turns slowly round it. It turns only on the card on show. */
.project-dial {
  flex: 1 1 0;
  min-height: 0;
  display: grid;
  place-items: center;
  border-bottom: var(--rule-w) solid var(--rule);
  container-type: size;
}
.dial {
  position: relative;
  width: min(100cqw, 100cqh);
  aspect-ratio: 1;
  border: var(--rule-w) solid var(--rule);
  border-radius: 50%;
}
.dial i {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: var(--rule-w);
  margin-left: calc(var(--rule-w) / -2);
  background: var(--rule);
  rotate: -45deg;
}
@media (prefers-reduced-motion: no-preference) {
  .dial i { animation: dial-turn 36s linear infinite; animation-play-state: paused; }
  .slide.is-current .dial i { animation-play-state: running; }
}
@keyframes dial-turn {
  from { rotate: -45deg; }
  to { rotate: 315deg; }
}
/* too little room for a circle worth the name: the cell keeps its place, empty */
@container (max-height: 56px) {
  .dial { display: none; }
}
/* the number and its afterimages, one on another, clipped to the cell (see dash() in script.js) */
.project-no { position: relative; display: grid; justify-items: start; overflow: hidden; }
.project-no > span { grid-area: 1 / 1; }
.no-echo {
  opacity: 0;
  pointer-events: none;
  color: hsl(var(--hue) 100% 64%);
  text-shadow: 0 0 14px hsl(var(--hue) 100% 60% / 0.55);
}
.slide:not([data-tone="dark"]) .no-echo { color: hsl(var(--hue) 90% 36%); text-shadow: none; }
.project-dial + div { padding: var(--cell); padding-bottom: calc(var(--cell) + var(--foot)); }
.project-dial + div:has(.picker) { padding-bottom: 0; }
.kicker { font-style: italic; color: var(--muted); margin-bottom: 8px; }
.project h2 {
  font-size: clamp(26px, 3vw, 48px);
  font-size: clamp(26px, min(3.3cqw, 7.4cqh), 56px);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 1;
  margin-bottom: 12px;
}
.desc { max-width: 28em; color: var(--soft); }
.desc strong { color: var(--ink); }
.desc a { text-decoration: underline; text-underline-offset: 2px; }

.picker {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  margin: var(--cell) calc(var(--cell) * -1) 0;
  padding: var(--cell);
  padding-bottom: calc(var(--cell) + var(--foot));
  border-top: var(--rule-w) solid var(--rule);
  container-type: inline-size;
}
/* Series pager: steps through the groups of thumbnails in a picker */
.pager { flex: 0 0 100%; display: flex; align-items: center; gap: 10px; }
.pager-step {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: color 0.6s ease, background-color 0.6s ease, scale 0.4s var(--spring);
}
.pager-step:hover { scale: 1.1; }
.pager-step:active { scale: 0.92; }
.pager-step svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.pager-label { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.pager-name { font-weight: 700; font-style: italic; letter-spacing: -0.02em; font-size: 15px; }
.pager-count { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The thumbnail picker. The script adds a ring that glides to the chosen thumbnail and,
   under a mouse, sets --near on each thumbnail (0 far from the cursor, 1 right under it)
   and moves a soft light to the cursor. */
.thumbs {
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  gap: 6px;
  padding: 6px;
  border-radius: 18px;
  transition: color 0.6s ease, background-color 0.6s ease, scale 0.5s var(--spring);
}
.thumbs.is-lit { scale: 1.03; }
/* Small thumbnails shrink as far as they must for six to share one row of the column */
.thumbs.is-small { --thumb: min(52px, (100cqw - 42px) / 6); }
.thumb {
  position: relative;
  z-index: 3;
  width: var(--thumb, 60px);
  aspect-ratio: 3 / 2;
  padding: 0;
  border: 0;
  border-radius: 12px;
  overflow: hidden;
  background: #111;
  cursor: pointer;
  opacity: calc(0.55 + var(--near, 0) * 0.4);
  scale: calc(1 + var(--near, 0) * 0.14);
  transition: opacity 0.25s ease, scale 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.25s ease;
}
.thumb:hover { opacity: 0.92; }
.thumb:active { scale: 0.93; transition-duration: 0.1s; }
.thumb.is-on { opacity: 1; box-shadow: 0 0 0 2px #fff, 0 4px 12px rgb(0 0 0 / 0.3); }
.has-ring .thumb.is-on { box-shadow: none; }
.thumbs-ring {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 4;
  border-radius: 12px;
  pointer-events: none;
  box-shadow: 0 0 0 2px #fff, 0 4px 12px rgb(0 0 0 / 0.3);
  translate: var(--rx, 0) var(--ry, 0);
  scale: calc(1 + var(--near, 0) * 0.14);
  transition: translate 0.76s var(--spring), scale 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.thumbs-glow {
  position: absolute;
  inset: 0;
  z-index: 5;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
}
.thumbs.is-lit .thumbs-glow { opacity: 1; }
.thumbs-glow i {
  position: absolute;
  left: -70px;
  top: -70px;
  width: 140px;
  height: 140px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(255 255 255 / 0.42), rgb(255 255 255 / 0.1) 60%, rgb(255 255 255 / 0));
  translate: var(--gx, 0) var(--gy, 0);
  transition: translate 0.14s ease-out;
}
/* Thumbnails pop in one after another whenever their series comes up */
@media (prefers-reduced-motion: no-preference) {
  .thumb[data-page] { animation: thumb-in 0.5s var(--spring) backwards; animation-delay: calc(var(--n, 0) * 45ms); }
}
@keyframes thumb-in {
  from { opacity: 0; scale: 0.7; }
}
@media (prefers-reduced-motion: reduce) {
  .thumbs, .thumb, .thumbs-ring { transition: none; }
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.stage-caption { font-style: italic; color: var(--muted); }

.stage-wrap { container-type: size; display: grid; place-items: center; min-width: 0; min-height: 0; padding: var(--cell); padding-bottom: calc(var(--cell) + var(--foot)); }
.stage {
  position: relative;
  width: min(100cqw, 150cqh);          /* the largest 3:2 box that fits */
  aspect-ratio: 3 / 2;
  border-radius: var(--r-in);
  overflow: hidden;
  background: #17120f;
  box-shadow: 0 24px 60px -22px rgb(0 0 0 / 0.6), 0 0 0 1px rgb(255 255 255 / 0.35);
}
/* A piece that is not on show is hidden outright once it has faded, so its picture is never decoded */
.shot { position: absolute; inset: 0; opacity: 0; visibility: hidden; transition: opacity 0.7s ease, visibility 0s linear 0.7s; }
.shot.is-on { opacity: 1; visibility: visible; transition-delay: 0s; }
.shot img, .shot video { width: 100%; height: 100%; object-fit: cover; }
/* Pictures that are not 3:2 are shown whole, on the dark of the stage */
.shot.is-whole img { object-fit: contain; }
/* The album stages hold the pieces themselves, cut out of their studio backdrops (see
   tools/albumcut.py): no box round them, and each picture is sized and placed by its own
   --w, --l, --t (as fractions of the stage) so the pieces fill the stage, centred. A loop keeps
   playing with its backdrop masked away by --mask. */
.stage.is-cutout { background: none; box-shadow: none; border-radius: 0; overflow: visible; }
.stage.is-cutout .shot > img, .stage.is-cutout .shot > video {
  position: absolute;
  left: calc(var(--l) * 100%);
  top: calc(var(--t) * 100%);
  width: calc(var(--w) * 100%);
  max-width: none;
  height: auto;
  aspect-ratio: var(--ratio);
  object-fit: fill;
}
/* a picture reaches past the stage, but only the pieces in it are there: its clear edges must not
   take the pointer from the words beside it */
.stage.is-cutout .shot > img, .stage.is-cutout .shot > video:not([controls]) { pointer-events: none; }
/* the pieces' own shadow, drawn by the page from their outline */
.stage.is-cutout .shot { filter: drop-shadow(0 18px 24px rgb(0 0 0 / 0.5)); }
.stage.is-cutout .shot > video {
  -webkit-mask: var(--mask) center / 100% 100% no-repeat;
  mask: var(--mask) center / 100% 100% no-repeat;
}
/* A piece in 3D (piece3d.js) sits over its picture with room round the stage to move into, and
   takes over from it once drawn, casting its own shadow. The picture stays, unseen, for its words. */
.piece-3d {
  position: absolute;
  left: calc(var(--margin) * -100%);
  top: calc(var(--margin) * -100%);
  width: calc((1 + 2 * var(--margin)) * 100%);
  height: calc((1 + 2 * var(--margin)) * 100%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
}
.stage.is-cutout .shot.is-3d { filter: none; }
.shot.is-3d > .piece-3d { opacity: 1; }
/* The prints (the design card) are one card for the whole stage, over every piece in it: the box
   round the stage gives way to the card and its shadow */
.stage.is-3d {
  background: none;
  box-shadow: none;
  border-radius: 0;
  overflow: visible;
  cursor: pointer;
  transition: background-color 0.35s ease, box-shadow 0.35s ease;
}
.stage.is-3d > .piece-3d { opacity: 1; }
.stage.is-3d > .shot > img { opacity: 0; transition: opacity 0.35s ease; }
.shot.is-3d > img, .shot.is-3d > video { opacity: 0; transition: opacity 0.35s ease; }

/* A film that plays elsewhere: its opening frame and a play button, linking to it */
.film {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 14px;
  color: #fff;
}
.film img {
  grid-area: 1 / 1;
  width: min(86%, 640px);
  height: auto;
  border-radius: 10px;
  box-shadow: 0 18px 40px -18px rgb(0 0 0 / 0.8);
  transition: scale 0.6s var(--spring);
}
.film-play {
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  --g-tint: rgb(12 12 14 / 0.5);
  transition: scale 0.5s var(--spring);
}
.film-play svg { width: 22px; height: 22px; fill: #fff; translate: 1px 0; }
.film:hover img, .film:focus-visible img { scale: 1.02; }
.film:hover .film-play, .film:focus-visible .film-play { scale: 1.12; }

/* ---------- Writing ---------- */
/* The articles are cells of their own, ruled apart, sharing the card's height */
.inline-link { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
.articles { display: grid; grid-auto-rows: minmax(min-content, 1fr); min-height: 0; }
.articles li + li { border-top: var(--rule-w) solid var(--rule); }
.articles a {
  display: grid;
  grid-template-columns: 5em minmax(0, 1fr) auto;
  gap: 16px;
  align-items: start;
  align-content: center;
  height: 100%;
  padding: var(--cell);
}
.articles li:last-child a { padding-bottom: calc(var(--cell) + var(--foot)); }
.article-kind { font-style: italic; color: var(--muted); padding-top: 0.55em; }
.article-body { display: grid; gap: 7px; }
.article-title {
  font-size: clamp(20px, min(2.5cqw, 5.6cqh), 40px);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 1;
  text-wrap: balance;
  text-decoration: underline transparent;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.14em;
  transition: text-decoration-color 0.25s ease;
}
.articles a:hover .article-title, .articles a:focus-visible .article-title { text-decoration-color: currentColor; }
.article-summary { max-width: 36em; color: var(--soft); }
.article-meta { font-size: 11px; color: var(--muted); }
.article-arrow {
  font-size: clamp(20px, min(2.5cqw, 5.6cqh), 40px);
  font-weight: 300;
  line-height: 1;
  transition: transform 0.25s ease;
}
.articles a:hover .article-arrow, .articles a:focus-visible .article-arrow { transform: translate(4px, -4px); }

/* ---------- Film reviews: two columns of three ruled cells ---------- */
.review-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: minmax(min-content, 1fr);
  min-height: 0;
}
.review-list li:nth-child(n + 3) { border-top: var(--rule-w) solid var(--rule); }
.review-list li:nth-child(even) { border-left: var(--rule-w) solid var(--rule); }
.review-list a {
  display: grid;
  align-content: center;
  gap: clamp(8px, 1.6cqh, 14px);
  height: 100%;
  padding: var(--cell);
}
.review-list li:nth-last-child(-n + 2) a { padding-bottom: calc(var(--cell) + var(--foot)); }
.review-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.review-film {
  font-size: clamp(20px, min(2.3cqw, 5cqh), 38px);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 1;
  text-wrap: balance;
  text-decoration: underline transparent;
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.14em;
  transition: text-decoration-color 0.25s ease;
}
.review-list a:hover .review-film, .review-list a:focus-visible .review-film { text-decoration-color: currentColor; }
.review-film small { margin-left: 6px; font-size: 0.42em; font-weight: 400; font-style: italic; letter-spacing: 0; color: var(--muted); }
.review-stars { flex: none; font-size: clamp(12px, min(1.1cqw, 2.4cqh), 17px); letter-spacing: 2px; color: var(--soft); }
.review-quote { max-width: 34em; color: var(--soft); font-size: clamp(14px, min(1.3cqw, 2.9cqh), 20px); line-height: 1.38; }

/* The whole review, shown in the card's left column while a review is pointed at. The card's own
   title and description fade out beneath it. */
.project-text { position: relative; }
.project-text > div { transition: opacity 0.25s ease; }
.project-text.is-reading > div:not(.review-panel) { opacity: 0; }
.project-text > .review-panel {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--cell) var(--cell) 48px;
  /* a long review fades out at the foot of the column, where it scrolls on */
  -webkit-mask-image: linear-gradient(#000 calc(100% - 56px), transparent);
  mask-image: linear-gradient(#000 calc(100% - 56px), transparent);
  scrollbar-width: thin;
  scrollbar-color: rgb(0 0 0 / 0.25) transparent;
  opacity: 0;
  visibility: hidden;
  translate: 0 12px;
  transition: opacity 0.25s ease, translate 0.4s var(--spring), visibility 0s linear 0.25s;
}
.project-text.is-reading > .review-panel { opacity: 1; visibility: visible; translate: 0 0; transition-delay: 0s; }
.review-panel-meta { font-style: italic; color: var(--muted); }
/* The parts follow one another as a flow rather than a column, so the title and then the review
   can run down the right of the film's poster and on beneath it */
.review-panel > * + * { margin-top: 0.9em; }
.review-poster {
  float: left;
  width: clamp(84px, 36%, 160px);
  height: auto;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  margin: 0 clamp(14px, 1.3cqw, 22px) 0.6em 0;
  border-radius: 4px;
  box-shadow: 0 14px 28px -14px rgb(0 0 0 / 0.55), 0 1px 3px rgb(0 0 0 / 0.14);
}
/* the title at the top, level with the top of the poster, and its rating just under it */
.review-panel .review-poster + h3 { margin-top: -0.1em; }
.review-panel h3 + .review-panel-meta { margin-top: 0.5em; }
.review-panel h3 {
  font-size: clamp(22px, min(2.1cqw, 4.8cqh), 36px);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 1;
  text-wrap: balance;
}
.review-panel h3 small { margin-left: 6px; font-size: 0.4em; font-weight: 400; font-style: italic; letter-spacing: 0; color: var(--muted); }
.review-panel p:not(.review-panel-meta) { color: var(--soft); font-size: clamp(13px, min(1.05cqw, 2.3cqh), 16px); line-height: 1.5; }
.review-panel .review-aside { font-style: italic; color: var(--muted); margin-top: 1.4em; }
.review-panel-link { display: inline-block; color: var(--muted); text-decoration: underline; text-underline-offset: 0.18em; padding-bottom: 4px; }
.review-panel-link:hover { color: var(--ink); }
/* the review being read keeps its title underlined */
.review-list li.is-read .review-film { text-decoration-color: currentColor; }

/* ---------- Contact ---------- */
.closing {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}
.closing-portrait { height: calc(100% - var(--cell) * 2 - var(--foot)); max-width: 38cqw; margin: var(--cell); margin-bottom: calc(var(--cell) + var(--foot)); }
/* beside it: the mark, the statement and the ways to get in touch, each in a cell of its own */
.closing-main { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; min-width: 0; border-left: var(--rule-w) solid var(--rule); }
.closing-mark { padding: var(--cell); border-bottom: var(--rule-w) solid var(--rule); text-align: right; font-size: clamp(14px, 1.7cqw, 26px); font-weight: 700; letter-spacing: -0.04em; }
.closing-main > .statement { align-self: center; padding: var(--cell); }
.closing-contact { display: grid; gap: 16px; justify-items: start; padding: var(--cell); padding-bottom: calc(var(--cell) + var(--foot)); border-top: var(--rule-w) solid var(--rule); }
.closing-id { display: grid; gap: 2px; }
.closing-id em { color: var(--muted); }
.actions { display: flex; flex-wrap: wrap; gap: 8px; }
.actions .pill { transition: transform 0.2s ease; }
.actions .pill:hover { transform: scale(1.04); }

/* ---------- Photocopy filter over the whole site ----------
   One layer of black toner (dust, stray specks, drum streaks, a shadow at the edge of the
   glass) and white dropouts, which only show over dark areas. It is plain black and white
   with transparency, so it needs no blending or filtering of the page underneath.
   --copy sets how heavy it is (0 turns it off). */
:root { --copy: 1; }
.copy {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  opacity: var(--copy);
  background-image: url(textures/voids.svg), url(textures/toner.svg), url(textures/specks.svg), url(textures/streaks.svg);
  background-size: 380px 380px, 300px 300px, 420px 420px, 640px 900px;
  box-shadow: inset 0 0 110px rgb(0 0 0 / 0.175), inset 0 0 12px rgb(0 0 0 / 0.2);
}
/* Harder contrast, the way a copier flattens tones, on the artwork and portraits only */
.stage, .frame { filter: contrast(calc(1 + 0.16 * var(--copy))); }
/* <html data-copy="mono"> turns the whole site into a black-and-white copy (heavier to draw).
   The switch on the cover sets it, and the browser remembers it for next time. */
html[data-copy="mono"] .copy { -webkit-backdrop-filter: grayscale(1); backdrop-filter: grayscale(1); }

/* ---------- The screen: a CRT's curved glass ----------
   The whole page bows out like the face of a cathode-ray tube. The #crt filter (index.html) shows
   each point of the screen the picture from a little further out, most at the corners
   (tools/crtmap.py), so the edges curve and the corners round away into the black around the glass.
   Over it, under the same curve, the glass itself: fine scanlines, a darker rim and the glare.
   The curve moves what is drawn, not what is clicked: near the corners a link is drawn a little
   inward of where it answers, so the scale (0.044, in the filter) stays gentle. Touch screens keep
   the glass but not the curve: a filter over the whole page, redrawn as a finger scrolls it, is
   more than a phone should be asked to do. */
html { background: #000; }
@media (hover: hover) and (pointer: fine) {
  body { filter: url(#crt); }
}
.crt-filter { position: absolute; width: 0; height: 0; }
.crt-glass {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  border-radius: 3% / 5%;
  background: repeating-linear-gradient(to bottom, rgb(0 0 0 / 0.1) 0 1px, transparent 1px 3px);
  box-shadow: inset 0 0 90px 12px rgb(0 0 0 / 0.42), inset 0 0 8px 2px rgb(0 0 0 / 0.55);
}
/* The glare: the room's light caught on the curved glass, from a window up and to the left. A soft
   bright spot near the top left, a sheen falling away from it, a long diagonal streak across the
   glass, and the bottom rim catching the light. It is drawn once and never changes, and it bends
   with the rest of the screen, as it lies on the same glass. */
.crt-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(22% 17% at 21% 13%, rgb(255 255 255 / 0.2), rgb(255 255 255 / 0.07) 50%, transparent 78%),
    radial-gradient(85% 75% at 24% 6%, rgb(255 255 255 / 0.1), rgb(255 255 255 / 0.03) 48%, transparent 72%),
    linear-gradient(118deg, transparent 41%, rgb(255 255 255 / 0.05) 47%, rgb(255 255 255 / 0.075) 50%, rgb(255 255 255 / 0.03) 55%, transparent 63%),
    radial-gradient(62% 9% at 50% 101%, rgb(255 255 255 / 0.09), transparent 75%);
}

/* ---------- The shot (shot.js): a figure on a red slab shoots the picked chapter word ----------
   Above the cards and the drawer, under the photocopy grain. The script sets --cut (how far in
   from the word's side the slab starts), --slide (0 in place, 1 off screen) and --shake. */
.cutin {
  --dir: 1;
  position: fixed;
  inset: 0;
  z-index: 45;
  overflow: hidden;
  pointer-events: none;
  visibility: hidden;
  translate: var(--shake, 0) 0;
}
.cutin.is-on { visibility: visible; }
.cutin.is-left { --dir: -1; }
/* the cut-in art: it fades out at its foot, where the picture was cut off */
.cutin-art {
  position: absolute;
  max-width: none;
  -webkit-mask-image: linear-gradient(to top, transparent, #000 16%);
  mask-image: linear-gradient(to top, transparent, #000 16%);
  filter: contrast(1.08) saturate(1.12);    /* a little more bite, which hides some of the softness */
}
.cutin-flash { position: absolute; opacity: 0; }
.cutin-shell {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 2px;
  background: linear-gradient(#c4161c 0 68%, #e8b04a 68%);
  box-shadow: 0 0 0 1px #050203;
  opacity: 0;
}
.cutin-slab {
  position: absolute;
  inset: 0;
  background: #e5191e;
  clip-path: polygon(calc(var(--cut) + 9vw) 0, 100% 0, 100% 100%, var(--cut) 100%, calc(var(--cut) + 3.4vw) 63%, calc(var(--cut) - 1.2vw) 57%);
  translate: calc(var(--slide, 1) * 105% * var(--dir)) 0;
}
/* a black band showing just ahead of the red edge */
.cutin-shade { background: #0b0b0b; translate: calc((var(--slide, 1) * 105% - 1.8vw) * var(--dir)) 0; }
.cutin.is-left .cutin-slab { scale: -1 1; }
/* On a phone, for a word in the upper part of the screen, the slab is a band rising from the
   foot of the screen instead, its slanted edge (--cut-y) stopping short of the word */
.cutin.is-tall .cutin-slab {
  scale: none;
  clip-path: polygon(0 calc(var(--cut-y) + 5vh), 100% calc(var(--cut-y) - 2vh), 100% 100%, 0 100%);
  translate: 0 calc(var(--slide, 1) * 105%);
}
.cutin.is-tall .cutin-shade { translate: 0 calc(var(--slide, 1) * 105% - 1.4vh); }
.cutin-tracer {
  position: absolute;
  height: 4px;
  margin-top: -2px;
  border-radius: 2px;
  transform-origin: 0 50%;
  background: linear-gradient(90deg, rgb(255 240 190 / 0), #fff3c4 35%, #fff);
  box-shadow: 0 0 10px 2px rgb(255 190 60 / 0.8);
  opacity: 0;
}
.cutin-spark { position: absolute; width: 120px; height: 120px; opacity: 0; }
.cutin-spark::before, .cutin-spark::after, .cutin-flash::before, .cutin-flash::after {
  content: "";
  position: absolute;
  clip-path: polygon(50% 0, 58% 30%, 82% 6%, 70% 36%, 100% 30%, 76% 50%, 100% 72%, 69% 64%, 80% 96%, 57% 70%, 48% 100%, 42% 70%, 16% 92%, 30% 62%, 0 66%, 24% 49%, 2% 26%, 32% 36%, 20% 4%, 44% 30%);
}
.cutin-spark::before { inset: 0; background: #e5191e; }
.cutin-spark::after { inset: 24%; background: #fff; rotate: 18deg; }
.cutin-flash::before { inset: 0; background: #ff3b14; }
.cutin-flash::after { inset: 22%; background: #fff3b0; rotate: 14deg; }

/* ---------- Motion (only when JS is running and motion is welcome).
   Titles type out; everything else slides up out of transparency. ---------- */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    transform: translateY(26px);
    transition: opacity 0.9s ease, transform 0.9s cubic-bezier(0.2, 0.7, 0.2, 1);
    transition-delay: calc(var(--delay, 0s) + var(--stagger, 0s));
  }
  .js .reveal.in { opacity: 1; transform: none; }

  .js .type { visibility: hidden; }
  .js .type.typing, .js .type.typed { visibility: visible; }
  .js .lockup h1:not(.is-still) { color: transparent; }
}

.untyped { opacity: 0; }
.caret { position: relative; }
.caret::after {
  content: "";
  position: absolute;
  left: 0.06em;
  top: 0.2em;
  bottom: 0.16em;
  width: max(2px, 0.035em);
  background: currentColor;
}
.typing.resting .caret::after { animation: blink 0.5s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }

/* ---------- Narrower cards: the CV dates sit above each entry ---------- */
@container (max-width: 1066px) {
  .cv li { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .cv .when br { display: none; }
}
/* A short card (a laptop screen, now that the sidebar takes some width): the About card
   closes up its spacing and puts each place on the same line as its organisation */
@container (max-height: 780px) {
  .card-body.about { --cell: clamp(14px, min(2cqw, 3.4cqh), 28px); }
  .about-main { gap: 18px; }
  .bio p + p { margin-top: 0.65em; }
  .side { gap: 16px; }
  .cv-col { gap: 16px; }
  .cv li + li { margin-top: 9px; }
  .cv .label, .tools .label { margin-bottom: 7px; }
  .cv li > div br:nth-of-type(2) { display: none; }
  .cv small::before { content: " · "; font-style: normal; }
  .tool-list { gap: 10px clamp(12px, 2cqw, 24px); }
  .tool-list li { gap: 6px; }
}

/* ---------- Phones: one column per card. Each card after the cover grows to hold all of its
   content, so the page scrolls straight through without a card scrolling inside itself. ---------- */
@media (max-width: 760px) {
  /* Each section is as tall as what it shows, ruled off from the one before, rather than a screen
     each, which left the shorter ones with most of a screen of empty glass below them. The cover is
     a screen, and so is the last, which the page ends on. Its cells stack, a rule between each. */
  .slide { height: auto; }
  .slide#top { height: 100vh; height: 100svh; }
  /* A section shorter than the screen can be read at the top of the screen while its middle, which
     the colour behind the glass follows, is already at the next: the words would sit on a blend of
     the two. So each section carries its own colour instead, the same one the glass gives it (its
     look's colour under the clear or smoked tint), and the rule above it marks the change. */
  .ambient { display: none; }
  .js .slide .card.glass { background: color-mix(in srgb, var(--tone) 67%, #fff); }
  .js .slide[data-tone="dark"] .card.glass { background: color-mix(in srgb, var(--tone) 31%, rgb(9 7 11)); }
  .slide:not(#top) .card { --cell: 20px; height: auto; container-type: inline-size; }
  .slide:not(#contact) .card { --foot: 0px; }
  .slide:not(#top) .card-body { height: auto; overflow: visible; }
  .slide#contact .card { min-height: 100vh; min-height: 100svh; }

  .index-link { left: calc(var(--mx) * 1%); top: calc(var(--my) * 1%); padding: 12px; }
  .film-play { width: 52px; height: 52px; }

  .about, .cv, .project, .closing { display: block; }
  .about-row { grid-template-columns: 34% minmax(0, 1fr); }
  .toolkit { row-gap: 26px; }
  .toolkit .app, .frameworks .app { width: 46px; }
  .toolkit .tool-list, .frameworks .tool-list { gap: 12px 11px; }
  /* the frameworks sit in the side column, after the CV, on a wide card; on a phone they follow the
     software and languages instead: the two columns give their pieces to the card, in order */
  .card-body.about { display: flex; flex-direction: column; }
  .about .statement { order: 1; }
  .about .about-row { order: 2; }
  .about .toolkit { order: 3; }
  .about .frameworks { order: 4; }
  .about .cv-col:first-child > :first-child { order: 5; }
  .about .cv-col:first-child > :last-child { order: 6; }
  .about .cv-col:last-child > :first-child { order: 7; }
  .about .cv-col:last-child > :last-child { order: 8; }
  .about .cv-col > div { border-left: 0; border-top: var(--rule-w) solid var(--rule); }

  .project-text { border-right: 0; }
  .project-dial { display: none; }
  .stage-wrap { container-type: normal; border-top: var(--rule-w) solid var(--rule); }
  .stage { width: 100%; }
  .articles, .review-list { border-top: var(--rule-w) solid var(--rule); }
  .articles, .review-list { grid-auto-rows: auto; }
  .review-list { grid-template-columns: minmax(0, 1fr); }
  .review-list li { border-left: 0; }
  .review-list li + li { border-top: var(--rule-w) solid var(--rule); }
  .articles a { grid-template-columns: minmax(0, 1fr) auto; }
  .article-kind { grid-column: 1 / -1; padding-top: 0; }

  .closing-portrait { height: auto; width: 52%; max-width: none; margin-bottom: var(--cell); }
  .closing-main { border-left: 0; border-top: var(--rule-w) solid var(--rule); }
  .closing-mark { text-align: left; }
}
