/* ==========================================================================
   style.css - single shared stylesheet
   Principle: HTML chrome never eases; only the canvas breathes.
   Palette is paper + ink + three grays. No border-radius, no box-shadow,
   no color, anywhere.
   Type is two voices: Satoshi carries reading text and headlines, the mono
   stack is the chrome voice (buttons, labels, indexes, captions). Headings
   press darker than body ink; body copy sits a half step lighter.
   Layers (source order matters, later layers only narrow earlier ones):
   1 tokens / 2 reset+base / 3 utilities / 4 chrome / 5 hairline grid
   6 page sections / 7 media + preferences + fallbacks (always last)
   ========================================================================== */

/* 1. tokens ------------------------------------------------------------- */
:root {
  --paper: #fafafa;
  --ink: #111111;
  --ink-head: #050505; /* headings press a step darker than ink */
  --ink-body: #3b3b3b; /* reading text, slightly lighter, still dark (9.9:1) */
  --panel: #ffffff; /* content panels lift slightly off the field */
  --gray-0: #f0f0f0; /* lightest tint: badge fields */
  --gray-1: #e4e4e4;
  --gray-2: #9c9c9c;
  --gray-3: #5c5c5c; /* darkest gray allowed for text: >= 4.5:1 on paper */
  /* the one non-monochrome mark on the site: the pointer. A Nothing-ish
     signal red, used for the cursor, a switch held off its default, the crop
     tick on the about page's photo cells, and the marks that annotate a case
     study's screenshots. Nothing else. */
  --accent: #d71921;
  --accent-wash: rgba(215, 25, 33, 0.14); /* --accent, laid over a screenshot */

  --hair: 1px;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --space-6: 4rem;
  --head-h: 3rem;
  --strip-h: 2.5rem;

  --font-sans: "Satoshi", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --text-xs: 0.625rem;
  --text-s: 0.75rem;
  --text-m: 0.875rem;
  --text-l: 1.25rem;
  --text-xl: clamp(1.5rem, 3vw, 2.25rem);
  --track: 0.08em;
  --track-wide: 0.18em;
}

/* 2. reset + base ------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

img,
canvas,
svg {
  display: block;
  max-width: 100%;
}

/* the display above would otherwise beat the hidden attribute */
img[hidden],
canvas[hidden],
svg[hidden] {
  display: none;
}

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

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-m);
  line-height: 1.55;
}

a {
  color: inherit;
}

button {
  font: inherit;
  font-family: var(--font-mono); /* controls speak in the chrome voice */
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

ul,
ol {
  list-style: none;
}

h1,
h2,
h3 {
  font-weight: 700;
  color: var(--ink-head);
  letter-spacing: var(--track);
  text-transform: uppercase;
}

::selection {
  background: var(--ink);
  color: var(--paper);
}

/* scrollbar as part of the drawing: a square ink thumb on a ruled track */
html {
  scrollbar-color: var(--ink) var(--paper); /* Firefox */
  scrollbar-width: thin;
}

::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

::-webkit-scrollbar-track {
  background: var(--paper);
  border-left: var(--hair) solid var(--ink);
}

::-webkit-scrollbar-thumb {
  background: var(--ink);
  border: 3px solid var(--paper); /* inset so the thumb reads as a bar */
}

::-webkit-scrollbar-thumb:hover {
  border-width: 2px;
}

::-webkit-scrollbar-corner {
  background: var(--paper);
}

:focus-visible {
  outline: var(--hair) solid var(--ink);
  outline-offset: 2px;
}

/* 3. utilities ----------------------------------------------------------- */
.label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

.idx {
  font-family: var(--font-mono);
  color: var(--gray-3);
  letter-spacing: var(--track);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.wrap {
  max-width: 1280px;
  margin-inline: auto;
  padding-inline: var(--space-4);
}

/* 4. chrome (subpages) --------------------------------------------------- */
.site-head {
  font-family: var(--font-mono);
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  min-height: var(--head-h);
  border-bottom: var(--hair) solid var(--ink);
  padding-inline: var(--space-4);
}

.site-head .wordmark {
  display: inline-flex;
  align-items: center;
  font-size: var(--text-s);
  letter-spacing: var(--track-wide);
  text-decoration: none;
}

.site-head .wordmark:hover {
  background: var(--ink);
  color: var(--paper);
}

.site-head nav {
  display: flex;
}

.site-head nav a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  padding: 0 var(--space-3);
  font-size: var(--text-s);
  letter-spacing: var(--track);
  text-decoration: none;
}

.site-head nav a:hover,
.site-head nav a[aria-current="page"] {
  background: var(--ink);
  color: var(--paper);
}

.site-head nav a:hover .idx,
.site-head nav a[aria-current="page"] .idx {
  color: var(--gray-2);
}

.head-strip {
  height: var(--strip-h);
  border-bottom: var(--hair) solid var(--ink);
  overflow: hidden;
}

.head-strip > div {
  width: 100%;
  height: 100%;
}

.site-foot {
  font-family: var(--font-mono);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-6);
  border-top: var(--hair) solid var(--ink);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

.site-foot nav {
  display: flex;
  gap: var(--space-3);
}

.site-foot a {
  text-decoration: none;
}

.site-foot a:hover {
  background: var(--ink);
  color: var(--paper);
}

/* 5. hairline grid --------------------------------------------------------
   Shared 1px borders without doubling: ink background bleeds through 1px
   gaps. Column counts are explicit per breakpoint; content counts (6, 12)
   divide evenly, because auto-fill with a partial row exposes an ink block. */
.hairline-grid {
  display: grid;
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.hairline-grid > * {
  background: var(--paper);
}

/* 6. page sections -------------------------------------------------------- */

/* 6a. landing */
.page-index {
  overflow: hidden;
}

.stage {
  position: fixed;
  inset: 0;
}

/* a sheet taller than the screen (the about page on a phone): the page
   scrolls it as one document. JS owns the exact height in px; putting the
   stage back in flow is what gives the body its scroll range. */
body.stage-scroll .stage {
  position: relative;
  inset: auto;
}

.grid-host {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.tile-nav {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

/* nav tiles are lattice cells: the canvas paints the fill AND the pixel-face
   labels; the anchor is the hit target and carries real text for assistive
   tech (visually hidden once the engine is confirmed alive, below) */
.tile-link {
  position: relative;
  font-family: var(--font-mono);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-3);
  pointer-events: auto;
  cursor: pointer;
  text-decoration: none;
  color: var(--paper);
  font-size: var(--text-l);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

/* engine-positioned: hidden until the first onTiles callback lands */
html.js .tile-link {
  position: absolute;
  top: 0;
  left: 0;
  visibility: hidden;
}

html.js body.tiles-ready .tile-link {
  visibility: visible;
}

/* the canvas draws the visible labels; keep the anchor text for screen
   readers only. Without JS (or with a dead engine) tiles-ready never lands,
   so these spans stay visible and the tiles remain plain text blocks. */
html.js body.tiles-ready .tile-link .tl-a,
html.js body.tiles-ready .tile-link .tl-b {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* focus ring: dashed hairline; focus also flips the tile to paper (the
   engine wipe), so the ring is drawn in ink to sit on the light cell */
.tile-link::after {
  content: "";
  position: absolute;
  inset: 6px;
  border: var(--hair) dashed transparent;
  pointer-events: none;
}

.tile-link:focus-visible {
  outline: none;
}

.tile-link:focus-visible::after {
  border-color: var(--ink);
}

/* wordmark + contact live in reserved lattice cells (kind: 'text') */
.grid-cell-overlay {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  padding: var(--space-3);
  pointer-events: none;
}

/* chrome cells speak mono; content cells (list, preview, bio) read in sans */
[data-cell="wordmark"],
[data-cell="contact"],
[data-cell="menu"] {
  font-family: var(--font-mono);
}

.grid-cell-overlay a {
  pointer-events: auto;
}

html.js .grid-cell-overlay {
  top: 0;
  left: 0;
  visibility: hidden;
}

/* an overlay only shows once the engine has actually placed it: pages may
   omit cells per breakpoint (e.g. the work preview on narrow screens) */
html.js body.tiles-ready .grid-cell-overlay.is-placed {
  visibility: visible;
}

/* page exit: chrome vanishes as the mold starts consuming the artwork.
   The mold is painted on the canvas, which sits under every overlay, so
   anything left visible would float above the transition. */
body.leaving .tile-nav,
body.leaving .grid-cell-overlay,
body.leaving .hover-img,
body.leaving .case-cell,
body.leaving .case-doc {
  visibility: hidden !important;
}

/* page arrival: the sheet starts fully consumed (black), chrome stays
   hidden until the engine has retreated the mold (onArrived) */
html.arriving body {
  background: var(--ink);
}

html.arriving .tile-nav,
html.arriving .grid-cell-overlay,
html.arriving .hover-img,
html.arriving .case-cell,
html.arriving .case-doc {
  visibility: hidden !important;
}

.grid-cell-overlay .wordmark {
  font-size: var(--text-m);
  letter-spacing: var(--track-wide);
}

.grid-cell-overlay .wordmark a {
  text-decoration: none;
}

.grid-cell-overlay .wordmark a:hover {
  background: var(--ink);
  color: var(--paper);
}

.grid-cell-overlay .tagline {
  margin-top: var(--space-1);
  color: var(--gray-3);
}

[data-cell="contact"] {
  justify-content: flex-end;
}

[data-cell="menu"] {
  align-items: flex-end;
}

[data-cell="contact"] nav,
[data-cell="menu"] nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

[data-cell="contact"] a,
[data-cell="menu"] a {
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-decoration: none;
  /* comfortable pointer/touch target without changing the visual weight */
  padding: var(--space-2) var(--space-1);
  margin: calc(-1 * var(--space-2)) calc(-1 * var(--space-1));
}

[data-cell="contact"] a:hover,
[data-cell="menu"] a:hover,
[data-cell="menu"] a[aria-current="page"] {
  background: var(--ink);
  color: var(--paper);
}

/* chrome labels re-set in the tiles' own 5x7 face (js/interactions.js swaps
   the text for an SVG from the engine's glyph map). Height rides the link's
   font-size, width follows the viewBox, and the fill is currentColor - so
   size, hover and aria-current all keep working from the rules above. */
.px-word {
  display: inline-block;
  height: 0.9em;
  width: auto;
  vertical-align: -0.05em;
}

/* hover-images easter egg host (enabled by listing images in landing.js) */
.hover-img {
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
  display: none;
  background-size: cover;
  background-position: center;
  pointer-events: none;
}

.hover-img.is-visible {
  display: block;
}

/* 6a2. work page: index + preview living inside the artwork ------------- */
.cell-list {
  /* stay clear of the panel's canvas-drawn hairline: without this inset the
     white rows paint straight over the border */
  padding: 1px;
  pointer-events: auto; /* the list scrolls internally if it outgrows its cell */
  overflow-y: auto;
}

/* the cell is a flex column: without this a list taller than its cell
   SHRINKS every row (clipping titles mid-line) instead of scrolling */
.cell-list > * {
  flex-shrink: 0;
}

/* row = a lattice cell that flips sign on hover, like the landing tiles.
   The ::before ink wipe sweeps in; text is white under mix-blend difference,
   so every glyph pixel inverts exactly as the edge passes it. */
.w-row {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3);
  border-bottom: var(--hair) solid var(--ink);
  text-decoration: none;
  cursor: pointer; /* ask rows are divs, and every row is an action */
  /* the overlay is a stacking context, so difference-blended text needs an
     in-context backdrop: the row owns its fill (panel white, no seam) */
  background: var(--panel);
}

.w-row::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 140ms cubic-bezier(0.33, 1, 0.68, 1);
}

.w-row:hover::before,
.w-row.is-current::before,
.w-row:focus-visible::before {
  transform: scaleX(1);
}

.w-row > * {
  position: relative;
  z-index: 1;
  mix-blend-mode: difference;
  color: #fff; /* difference: reads ink on paper, paper on ink */
}

.w-row .idx,
.w-row .year {
  color: var(--gray-2); /* difference keeps this gray in both polarities */
}

.w-row h2 {
  font-size: var(--text-m);
}

.w-row .year {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

.w-row:focus-visible {
  outline: none;
}

/* a project with no case page of its own still reads as one line in the list,
   exactly like the rest: the preview pane tells it on a wide screen, and on a
   narrow one tapping the row raises the toast, which carries the same sentence
   and the same mail link. The note under the title is the stand-in for both,
   so it appears only where neither can run: without JS, or on a failed engine
   (work.js bails before it wires the toast). */
.w-row--ask .w-note {
  display: none;
  grid-column: 2 / -1;
  margin-top: var(--space-2);
  font-size: var(--text-s);
}

html:not(.js) .w-row--ask .w-note,
html.engine-failed .w-row--ask .w-note {
  display: block;
}

/* badges: small mono chips on a gray field with an ink rule. Tags and the
   WIP mark share the one design. */
.badge {
  display: inline-block;
  padding: 2px var(--space-2);
  background: var(--gray-0);
  border: var(--hair) solid var(--ink);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.w-badge {
  margin-left: var(--space-2);
}

/* inside a work row every pixel goes through the difference blend, so the
   chip is painted in blend space: #0f renders as gray-0 on a paper row,
   #fff as ink, and both invert with the row's hover wipe */
.w-row .w-badge {
  background: #0f0f0f;
  border-color: #fff;
  color: #fff;
}

html:not(.js) .w-row .w-badge,
html.engine-failed .w-row .w-badge {
  background: var(--gray-0);
  border-color: var(--ink);
  color: var(--ink);
}

.w-row--ask .w-ask {
  display: block;
  width: max-content;
  max-width: 100%;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* the index is not finished and says so, in a row's rhythm but without a
   row's ink wipe: nothing here to open */
.w-more {
  padding: var(--space-3);
  background: var(--panel);
  font-family: var(--font-mono);
  color: var(--gray-2);
}

/* toast: one line of status in an ink tile, low on the sheet. It appears
   with the same two-frame step as everything else and leaves on its own. */
.toast {
  position: fixed;
  left: 50%;
  bottom: var(--space-5);
  transform: translateX(-50%);
  z-index: 9998;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  max-width: min(92vw, 36rem);
  padding: var(--space-2) var(--space-3);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  visibility: hidden;
}

.toast.is-live {
  visibility: visible;
  animation: boot 200ms steps(2, end) both;
}

.toast a {
  color: var(--paper);
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
}

.toast a:hover,
.toast a:focus-visible {
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  outline: none;
}

.w-row::after {
  content: "";
  position: absolute;
  inset: 5px;
  z-index: 2;
  border: var(--hair) dashed transparent;
  pointer-events: none;
}

.w-row:focus-visible::after {
  border-color: var(--paper);
}

.cell-preview {
  padding: 1px; /* same inset: keep the hairline visible */
}

/* the thumb stacks three layers for one project: its shot, the generative
   card that stands in when there is no shot yet, and the reveal clip that
   plays over the shot the first time it is previewed. Only one of the first
   two is ever mounted, so they never fight; the clip sits above both. */
.cell-preview .pv-thumb {
  position: relative;
  overflow: hidden;
  /* 4:3 always, so every embed is framed against one known ratio instead of a
     box that reshapes with the window. sizeThumb() in js/work.js writes the
     real pixel size, fitting the cell on both axes; these are the values that
     hold until it runs. Centred, since it is often narrower than the cell. */
  aspect-ratio: 4 / 3;
  flex: none;
  align-self: center;
  width: 100%;
  border-bottom: var(--hair) solid var(--ink);
}

.cell-preview .pv-card,
.cell-preview .pv-hero,
.cell-preview .pv-hero-clip,
.cell-preview .pv-reveal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* a live page as the shot: the frame lays the page out at its own size and
   js/work.js scales the region worth showing to the cell. Decorative, so it
   takes neither the pointer nor the tab order. */
.cell-preview .pv-embed {
  position: absolute;
  top: 0;
  left: 0;
  border: 0;
  transform-origin: 0 0;
  pointer-events: none;
}

/* a page that sizes itself is simply given the frame: no scaling, so it is
   pixel-exact at every breakpoint the lattice hands it */
.cell-preview .pv-embed--fluid {
  width: 100%;
  height: 100%;
}

/* a still is a glance, not a document figure: it fills the cell and crops,
   where a case study's .img-slot would letterbox it whole */
.cell-preview .pv-hero {
  object-fit: cover;
  background: var(--panel);
}

/* the clips are shown whole instead. A cropped screenshot still reads as a
   screenshot; a cropped animation reads as broken, with shapes leaving the
   cell on every side. Both of Poka's clips are padded to the cell's ratio on
   their own paper ground, so whole and filled are the same thing here, and a
   clip of any other shape simply letterboxes onto matching paper. */
.cell-preview .pv-hero-clip,
.cell-preview .pv-reveal {
  object-fit: contain;
  background: var(--paper);
}

/* a column: the identity sits at the top and the reference link and tags at
   the foot, both always in view. Only the description gives way when the
   cell is short, so nothing that names or links the project is ever lost
   below a fold with no scrollbar to announce it. */
.cell-preview .pv-info {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  overflow: hidden;
  padding: var(--space-3);
}

.cell-preview .pv-info h2 {
  display: inline;
  font-size: var(--text-m);
}

/* shrink-and-scroll, never grow: a short description stays with its title
   instead of stretching the column to push the tags down */
.cell-preview .pv-info .desc {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin-top: var(--space-2);
  font-size: var(--text-s);
}

/* a reference out of the sheet: plain text, underlined, inverting under the
   pointer like every other link on the site */
/* an explicit display would beat the hidden attribute's own display:none,
   and a project with no reference would keep showing the last one's */
.cell-preview .pv-info .pv-link[hidden] {
  display: none;
}

.cell-preview .pv-info .pv-link {
  display: inline-block;
  flex: none;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-underline-offset: 2px;
}

.cell-preview .pv-info .pv-link:hover,
.cell-preview .pv-info .pv-link:focus-visible {
  background: var(--ink);
  color: var(--paper);
  text-decoration: none;
  outline: none;
}

/* tags read as a row of badges under the description */
.cell-preview .pv-info .tags {
  display: flex;
  flex: none;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin-top: var(--space-2);
}

/* 6b. shared subpage title row */
.title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding-block: var(--space-5) var(--space-4);
}

.title-row h1 {
  font-size: var(--text-xl);
  font-weight: 900;
}

.title-row .count {
  color: var(--gray-3);
}

/* 6c. work cards */
.card-grid {
  grid-template-columns: repeat(3, 1fr);
}

.card .thumb {
  aspect-ratio: 4 / 3;
  border-bottom: var(--hair) solid var(--ink);
  overflow: hidden;
}

.card-meta {
  padding: var(--space-3);
}

.card-meta .idx {
  font-size: var(--text-xs);
}

.card-meta h2 {
  margin-top: var(--space-1);
  font-size: var(--text-m);
}

.card-tags {
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  color: var(--gray-3);
}

/* 6d. art gallery */
.tag-row {
  display: none; /* filter is a JS feature; shown under html.js below */
}

html.js .tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.tag-row button {
  background: var(--paper);
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

.tag-row button:hover,
.tag-row button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

/* 6e. about page: bio panel + photo cells inside the artwork ----------- */
.cell-bio {
  /* stay clear of the panel's canvas-drawn hairline */
  padding: 1px;
  pointer-events: auto;
  overflow-y: auto;
}

/* the phone's fun-fact drawer opens upward over the bio: the sheet text
   steps aside while covered, or it would paint across the drawer's ink.
   (Outweighs the tiles-ready is-placed rule that turns overlays visible.) */
html.js body.tiles-ready .grid-cell-overlay.cell-bio.is-covered {
  visibility: hidden;
}

.cell-bio .bio-body {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  /* no measure cap here: the grid cell is the measure, so the copy runs the
     full width of the panel it sits in (the no-JS fallback caps it instead,
     since there the cell has no engine-assigned width) */
  padding: var(--space-4);
}

.cell-bio p + p {
  margin-top: var(--space-3);
}

.cell-bio p {
  color: var(--ink-body);
}

.cell-bio .bio-hey {
  font-size: var(--text-l);
  font-weight: 700;
  color: var(--ink-head);
  letter-spacing: var(--track);
  margin-bottom: var(--space-2);
}

.cell-bio .bio-line .idx {
  display: inline-block;
  min-width: 5.5rem;
}

.cell-bio .bio-colophon {
  margin-top: auto;
  padding-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--gray-3);
}

/* fun facts: its own black lattice tile, sibling to RESUME and LINKEDIN.
   Closed, the canvas draws the tile and its pixel-face title; clicking grows
   the cell open (engine tile animation) and the drawer inside appears: the
   fact, PREVIOUS / NEXT, and a square slot for a supporting image.
   Without JS the tile is a plain bordered block with every fact visible. */
.fun-deck {
  margin-block: var(--space-5) var(--space-4);
  border: var(--hair) solid var(--ink);
}

/* engine-placed cell: the canvas paints the ink fill, so the overlay is
   transparent and only carries text - like the other tiles */
html.js .cell-funfact {
  margin: 0;
  border: 0;
  padding: 0;
  pointer-events: auto;
  color: var(--paper);
  overflow: hidden;
}

.ff-head {
  background: var(--ink);
  color: var(--paper);
}

/* closed: the button is the whole cell (canvas draws the label) */
html.js .cell-funfact .ff-head {
  background: none;
  height: 100%;
}

.ff-toggle {
  display: flex;
  width: 100%;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  text-align: left;
}

html.js .cell-funfact .ff-toggle {
  position: relative;
  height: 100%;
  padding: 0;
  cursor: pointer;
}

/* dashed paper focus ring, as on the nav tiles (ink on ink is nothing) */
html.js .cell-funfact .ff-toggle::after {
  content: "";
  position: absolute;
  inset: 6px;
  border: var(--hair) dashed transparent;
}

html.js .cell-funfact .ff-toggle:focus-visible {
  outline: none;
}

html.js .cell-funfact .ff-toggle:focus-visible::after {
  border-color: var(--paper);
}

/* open: the title button steps aside for the drawer */
html.js .cell-funfact.is-open .ff-head {
  display: none;
}

html.js .cell-funfact .tl-a {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

/* the drawer: fact + controls, with the image slot at the far end. It lives
   in a single lattice row, so the padding and gaps stay tight - the width is
   what the tile spends when it opens, never the height. */
html.js .cell-funfact .ff-body {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: var(--space-2) var(--space-3) var(--space-1);
  gap: var(--space-1);
}

html.js .cell-funfact .ff-body[hidden] {
  display: none;
}

.ff-cue {
  display: none; /* REVEAL / the counter only mean something under JS */
  font-family: var(--font-mono);
  color: var(--gray-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

html.js .ff-cue {
  display: block;
}

.ff-list {
  position: relative;
}

/* the whole card is the step target; PREV/NEXT and the ticks stay the
   keyboard route, so the pointer shortcut owes nothing to assistive tech */
html.js .ff-list {
  cursor: pointer;
}

html.js .ff-list::after {
  content: "";
  position: absolute;
  left: 6px;
  bottom: 6px;
  width: 8px;
  height: 8px;
  border-left: var(--hair) solid var(--gray-2);
  border-bottom: var(--hair) solid var(--gray-2);
  pointer-events: none;
}

html.js .ff-list:hover::after {
  border-color: var(--ink);
}

.ff-fact {
  padding: var(--space-3);
}

/* a floor for the single visible card: stepping must not shuffle the panel */
html.js .ff-fact {
  min-height: 8.5rem;
}

.ff-fact h3 {
  font-family: var(--font-mono);
  font-size: var(--text-m);
}

.ff-fact p {
  margin-top: var(--space-2);
  font-size: var(--text-m);
  line-height: 1.7;
}

.ff-fact a {
  text-decoration: underline;
}

.ff-fact a:hover {
  background: var(--ink);
  color: var(--paper);
}

/* the step happens in two frames, like the page boot: mechanical, not eased */
@keyframes ff-step {
  from {
    opacity: 0;
  }
}

html.js .ff-fact:not([hidden]) {
  animation: ff-step 160ms steps(2, end) both;
}

/* the window: fact text, then a square slot for a supporting image */
html.js .ff-window {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: stretch;
  gap: var(--space-3);
  cursor: pointer; /* the whole card steps forward */
}

/* inside the ink drawer everything inverts: paper text, paper rules */
html.js .cell-funfact .ff-window {
  flex: 1;
  min-height: 0;
}

html.js .cell-funfact .ff-list {
  overflow-y: auto; /* long facts scroll rather than burst the cell */
}

html.js .cell-funfact .ff-list::after {
  display: none; /* the drawer is already open: no reveal hint needed */
}

html.js .cell-funfact .ff-fact {
  padding: 0;
  min-height: 0;
}

html.js .cell-funfact .ff-fact h3 {
  color: var(--gray-2);
}

/* the band is one row tall, so the longest fact has to earn its lines - and
   a measure, so a wide screen does not stretch one sentence across the sheet */
html.js .cell-funfact .ff-fact p {
  max-width: 74ch;
  line-height: 1.6;
}

html.js .cell-funfact .ff-fact a:hover {
  background: var(--paper);
  color: var(--ink);
}

/* One row tall and square, sized by the band rather than by the picture. The
   image is out of flow (above), so width can follow the height through the
   aspect ratio without the file's own dimensions ever reaching the column. */
html.js .cell-funfact .ff-shot {
  align-self: stretch;
  flex: none;
  width: auto;
  height: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  border-color: var(--paper);
  background:
    repeating-conic-gradient(var(--gray-3) 0% 25%, var(--ink) 0% 50%) 0 0 / 14px 14px;
}

html.js .cell-funfact .ff-shot-label {
  background: var(--ink);
  border-color: var(--paper);
  color: var(--gray-2);
}

html.js .cell-funfact .ff-bar {
  margin-top: 0; /* the drawer's own gap already separates it */
  padding-top: var(--space-2);
}

html.js .cell-funfact .ff-step,
html.js .cell-funfact .ff-close {
  border: var(--hair) solid var(--paper);
  color: var(--paper);
  background: none;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

html.js .cell-funfact .ff-step:hover,
html.js .cell-funfact .ff-close:hover {
  background: var(--paper);
  color: var(--ink);
}

html.js .cell-funfact .ff-close {
  margin-left: auto;
}

html.js .cell-funfact .ff-tick::before {
  border-color: var(--paper);
}

html.js .cell-funfact .ff-tick[aria-current="true"]::before {
  background: var(--paper);
}

.ff-shot {
  display: none;
}

html.js .ff-shot {
  position: relative; /* the picture is laid out against this, not inside it */
  display: grid;
  place-items: center;
  align-self: center;
  flex: none;
  width: 8.5rem;
  aspect-ratio: 1;
  overflow: hidden;
  border: var(--hair) solid var(--ink);
  background:
    repeating-conic-gradient(var(--gray-1) 0% 25%, var(--panel) 0% 50%) 0 0 / 14px 14px;
}

/* The slot is the frame: the picture fills it and is cropped to it. Taken out
   of flow on purpose, so a large file can never contribute its own width to
   the slot and push the layout open - the frame sizes itself, always. */
.ff-shot img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ff-shot-label {
  padding: 2px var(--space-1);
  background: var(--panel);
  border: var(--hair) solid var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  color: var(--gray-3);
}

.ff-bar {
  display: none; /* stepping is a JS feature, like the art page's tag row */
}

html.js .ff-bar {
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-3);
  border-top: var(--hair) solid var(--ink);
  padding-top: var(--space-2);
}

/* the steps are ink blocks, like the landing's tiles */
.ff-step {
  flex-shrink: 0; /* never let the labels wrap to one letter per line */
  padding: var(--space-2) var(--space-3);
  background: var(--ink);
  color: var(--paper);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  white-space: nowrap;
}

.ff-step:hover {
  background: var(--paper);
  color: var(--ink);
  outline: var(--hair) solid var(--ink);
}

.ff-count {
  display: flex;
  align-items: center;
  font-family: var(--font-mono);
  color: var(--gray-3);
  white-space: nowrap;
}

/* ticks are lattice cells: hairline squares, the current one inked */
.ff-ticks {
  display: flex;
  flex: 1;
  min-width: 0; /* the row must never outgrow the panel: PREV/NEXT stay put */
  align-items: center;
  justify-content: center;
  gap: 2px;
}

.ff-tick {
  display: grid;
  place-items: center;
  width: 22px;
  height: 100%;
  min-height: 28px;
}

.ff-tick::before {
  content: "";
  width: 9px;
  height: 9px;
  border: var(--hair) solid var(--ink);
}

.ff-tick:hover::before {
  background: var(--gray-2);
}

.ff-tick[aria-current="true"]::before {
  background: var(--ink);
}

.photo-overlay {
  padding: 0;
}

.photo-cell {
  position: absolute;
  inset: 0;
  pointer-events: auto;
  cursor: pointer;
  /* a spot can be an anchor (the art page: click opens the piece elsewhere):
     the cell look stays identical to the button spots */
  display: block;
  color: inherit;
  text-decoration: none;
}

/* dashed focus ring, tile-link recipe (ink: it sits on a paper cell) */
.photo-cell::after {
  content: "";
  position: absolute;
  inset: 6px;
  border: var(--hair) dashed transparent;
  pointer-events: none;
}

.photo-cell:focus-visible {
  outline: none;
}

.photo-cell:focus-visible::after {
  border-color: var(--ink);
}

/* hidden until hover/focus/tap; checker backdrop is the placeholder, the
   real photo covers it once the asset exists */
.photo-reveal {
  position: absolute;
  inset: 0;
  visibility: hidden;
  border: var(--hair) solid var(--ink);
  background: repeating-conic-gradient(var(--ink) 0% 25%, var(--panel) 0% 50%) 0 0 / 16px 16px;
}

.photo-cell:hover .photo-reveal,
.photo-cell:focus-visible .photo-reveal,
.photo-cell.is-revealed .photo-reveal {
  visibility: visible;
}

/* touch has no hover to find the hidden pieces with: placed spots arrive
   already revealed, and empty boxes never read as broken. Gated on
   is-placed so a spot dropped by a narrow rebuild stays gone (visibility:
   visible on the reveal would otherwise beat the hidden overlay above it). */
@media (hover: none), (pointer: coarse) {
  html.js body.tiles-ready .grid-cell-overlay.is-placed .photo-reveal {
    visibility: visible;
  }
}

.photo-reveal .ph-label {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 2px 6px;
  background: var(--panel);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

/* the cell is the frame: the photo fills it edge to edge and is cropped to
   fit, never letterboxed and never stretched. --crop (set per image in the
   markup) picks which horizontal band of a tall source survives the crop -
   these are portrait shots in cells that are rarely portrait.
   The photos keep their own colour: they are the one place on the sheet where
   something outside the b&w system is allowed to show through. */
.photo-reveal img,
.photo-reveal video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% var(--crop, 35%);
}

/* a spot that carries sound says so: the corner mark is the only hint that
   a click will do more than pin the reveal. It goes quiet once sound is on. */
.photo-cell[data-sound] .photo-reveal::after {
  content: "";
  position: absolute;
  right: 6px;
  top: 6px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

.photo-cell.is-sounding .photo-reveal::after {
  background: var(--panel);
  box-shadow: 0 0 0 var(--hair) var(--ink);
}

/* 6g. case study: a document scrolling over the living lattice ---------- */
.page-case {
  background: var(--paper);
}

.case-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
}

/* chrome and index are lattice cells (js/case.js places them from onTiles),
   fixed so they hold position while the document scrolls underneath.
   Flush to the grid lines: no margins, no gaps, hairline rules. */
/* the canvas draws these cells (paper fill + rule); the HTML only carries
   the text, exactly as on the artwork sheets - so no white box of its own */
.case-cell {
  position: fixed;
  z-index: 4;
  font-family: var(--font-mono);
  top: 0;
  left: 0;
  visibility: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding: var(--space-3);
}

html.js body.tiles-ready .case-cell.is-placed {
  visibility: visible;
}

/* fallback when the lattice is too coarse to give a cell its own span
   (few columns): the chrome becomes the same top row the artwork pages
   have - wordmark cell left, menu cell right, one lattice row tall, a
   hairline between them. Part of the document rather than a frame
   (position absolute), so it scrolls away with the page instead of
   following the reader down. */
body.tiles-ready .case-cell[data-cell="wordmark"]:not(.is-placed),
body.tiles-ready .case-cell[data-cell="menu"]:not(.is-placed) {
  visibility: visible;
  position: absolute;
  top: 0;
  height: var(--lat-ch, 190px);
  padding: var(--space-2);
  /* no canvas cell behind an unplaced overlay: it supplies its own paper */
  background: var(--panel);
  border-bottom: var(--hair) solid var(--ink);
}

body.tiles-ready [data-cell="wordmark"]:not(.is-placed) {
  left: 0;
  right: 50%;
  border-right: var(--hair) solid var(--ink);
}

body.tiles-ready [data-cell="menu"]:not(.is-placed) {
  left: 50%;
  right: 0;
}

/* same for the index: unplaced means it spans the foot of the viewport */
body.tiles-ready .case-index:not(.is-placed) {
  visibility: visible;
  top: auto;
  bottom: 0;
  left: 0;
  width: 100%;
  height: auto;
  background: var(--panel);
  border-top: var(--hair) solid var(--ink);
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0;
}

body.tiles-ready .case-index:not(.is-placed) .ci-list {
  flex-direction: row;
  overflow-x: auto;
  scrollbar-width: none;
}

body.tiles-ready .case-index:not(.is-placed) .ci-list::-webkit-scrollbar {
  display: none;
}

/* same size as the artwork pages' wordmark cell: navigating into a case
   study must not resize the chrome */
.case-cell .wordmark {
  font-size: var(--text-m);
  letter-spacing: var(--track-wide);
}

.case-cell .wordmark a {
  text-decoration: none;
}

.case-cell .tagline {
  margin-top: 2px;
  color: var(--gray-3);
}

/* menu items keep their own height (a stretched row made the active chip
   as tall as the whole lattice cell) */
[data-cell="menu"] {
  flex-direction: row;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--space-1);
}

[data-cell="menu"] a {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-decoration: none;
}

.case-cell a:hover,
[data-cell="menu"] a[aria-current="true"] {
  background: var(--ink);
  color: var(--paper);
}

/* section index: a lattice cell listing the sections, with a reading meter */
.case-index {
  justify-content: flex-start;
  padding: 0;
  overflow: hidden;
  background: var(--panel);
  border: var(--hair) solid var(--ink);
}

.ci-list {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* while js/case.js measures the cell, the list must take its natural
   height instead of collapsing as a zero-basis flex child */
.case-index.is-measuring .ci-list {
  flex: none;
  overflow: visible;
}

.ci-list a {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 7px var(--space-3) 7px var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-decoration: none;
  white-space: nowrap;
}

.ci-num {
  color: var(--gray-2);
  font-variant-numeric: tabular-nums;
}

.ci-list a:hover,
.ci-list a.is-current {
  background: var(--ink);
  color: var(--paper);
}

.ci-list a:hover .ci-num,
.ci-list a.is-current .ci-num {
  color: var(--gray-2);
}

/* JS writes --p (0..1); each breakpoint picks the axis it fills along */
/* the reading column continues the lattice: its edges sit on column lines
   (js/case.js writes --doc-x / --doc-w from the engine's own geometry) */
.case-doc {
  position: relative;
  z-index: 2;
  width: var(--doc-w, min(760px, 100%));
  margin-left: var(--doc-x, 0);
  padding-top: var(--doc-top, var(--space-6));
  padding-bottom: calc(var(--lat-ch, 190px) + var(--space-5));
  display: flex;
  flex-direction: column;
  gap: var(--space-4); /* the artwork shows through between the boxes */
  counter-reset: sec; /* numbers the sections, matching the index cell */
}

/* no channel (phones, small tablets): the document IS the page. Full
   width, flush under the header row, no artwork in the margins. */
body:not(.has-channel) .case-doc {
  width: 100%;
  margin-left: 0;
}

/* each section is its own box on the sheet: the gap between boxes does the
   dividing, so no rules are drawn inside or between them */
.case-panel {
  /* a length, not a percentage: full-bleed children cancel it exactly */
  --pad-x: clamp(var(--space-4), 2.5vw, var(--space-6));
  background: var(--panel);
  border: var(--hair) solid var(--ink);
  padding: var(--space-6) var(--pad-x);
  counter-increment: sec;
}

/* the index cell is canvas-drawn */
body.has-channel .case-index.is-placed {
  background: none;
}

.case-panel > * + * {
  margin-top: var(--space-4);
}

/* body copy: the one place on the site tuned for reading, not for pattern.
   The measure is the column itself - text and rules end on the same line. */
.case-panel p {
  font-size: 0.9375rem;
  line-height: 1.75;
  color: var(--ink-body);
}

.case-panel p + p {
  margin-top: var(--space-4);
}

/* emphasis inside reading text presses back to headline black */
.case-panel p strong {
  color: var(--ink-head);
}

/* section header: the type does the dividing now, no rule needed. Big,
   black, and fronted by a small mono index that echoes the section list. */
.case-h {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: clamp(1.55rem, 3.4vw, 2.4rem);
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: 0.005em;
  color: var(--ink-head);
  margin-bottom: var(--space-5);
}

.case-h::before {
  content: counter(sec, decimal-leading-zero);
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-s);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  color: var(--gray-2);
}

/* hero */
.case-hero {
  padding-block: var(--space-6);
}

.case-hero h1 {
  font-size: clamp(2.25rem, 5.5vw, 3.6rem);
  font-weight: 900;
  line-height: 1.02;
  letter-spacing: 0.01em;
  margin-top: var(--space-2);
}

.case-hero .idx {
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

.case-sub {
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  font-weight: 500;
  line-height: 1.35;
  margin-top: var(--space-4);
  color: var(--gray-3);
}

/* the lede reads a half size up from body copy, no rule above it */
.case-panel .case-lede {
  font-size: 1.125rem;
  line-height: 1.7;
  font-weight: 500;
}

/* explicit columns: with auto-fit, an incomplete last row exposes the ink
   background as a solid block (4 facts / 3 metrics divide evenly here) */
.case-facts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.case-facts > div {
  background: var(--panel);
  padding: var(--space-3);
}

.case-facts dt {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  color: var(--gray-3);
}

.case-facts dd {
  margin-top: var(--space-1);
  font-size: var(--text-s);
  font-weight: 500;
}

/* metric row */
.case-metrics {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.case-metrics li {
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-4) var(--space-3);
}

.case-metrics strong {
  display: block;
  font-size: clamp(1.75rem, 3.2vw, 2.6rem);
  font-weight: 900;
  letter-spacing: 0;
}

.case-metrics span {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--gray-2);
}

.case-quote {
  border-left: var(--hair) solid var(--ink);
  padding: var(--space-3) 0 var(--space-3) var(--space-4);
  font-size: clamp(1.15rem, 2.2vw, 1.5rem);
  font-weight: 700;
  line-height: 1.4;
  color: var(--ink-head);
}

.case-list {
  font-size: 0.9375rem;
  line-height: 1.7;
  color: var(--ink-body);
}

/* list items carry a lattice-cell bullet instead of a rule below */
.case-list li {
  position: relative;
  padding-block: var(--space-2);
  padding-left: var(--space-4);
}

.case-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.95em;
  width: 8px;
  height: 8px;
  background: var(--ink);
}

/* numbered flow steps: air between them, no rule */
.case-step + .case-step {
  margin-top: var(--space-6);
}

/* steps are named, not numbered: the heading carries the whole weight */
.case-step h3 {
  font-size: clamp(1.2rem, 2.2vw, 1.5rem);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.case-step p {
  margin-top: var(--space-3);
  margin-bottom: var(--space-5);
}

.case-step .img-slot {
  margin-top: var(--space-3);
}

/* decisions: what -> why, split by air and weight instead of rules */
.case-decisions > div + div {
  margin-top: var(--space-5);
}

.case-decisions dt {
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-head);
}

.case-decisions dd {
  margin-top: var(--space-2);
  font-size: 0.9375rem;
  line-height: 1.75;
  color: var(--ink-body);
}

/* image + video slots: checker placeholder in the engine's language,
   swap the inner div for <img>/<video> when the assets land.
   Inside a case panel they are bands, not cards: they run edge to edge and
   the hairline above and below is the only frame they get. */
/* a slot holds the real <img> / <video>: drop a file at the path in the
   markup and it appears. Until then the media fails to load, js/case.js
   marks the slot empty, and the checker + label below show instead. */
.img-slot {
  position: relative;
  aspect-ratio: var(--slot-ratio, 16 / 9);
  display: grid;
  place-items: center;
  border: var(--hair) solid var(--ink);
  background:
    repeating-conic-gradient(var(--gray-1) 0% 25%, var(--panel) 0% 50%) 0 0 / 16px 16px;
}

.img-slot img,
.img-slot video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain; /* screenshots must not be cropped */
  background: var(--ink);
}

/* video keeps its own aspect: let the element letterbox inside the slot */
.img-slot video {
  object-fit: contain;
}

/* a clip answers a click by holding still (js/case.js), so it is a control:
   focusable, and it says so on focus in the same dashed hairline the tiles
   and photo cells use. Paper, because it sits on the clip's ink ground. */
.img-slot video[data-autoplay]:focus-visible {
  outline: var(--hair) dashed var(--paper);
  outline-offset: -6px;
}

/* the label is the empty state only */
.img-slot:not(.is-empty) .slot-label {
  display: none;
}

.img-slot.is-empty img,
.img-slot.is-empty video {
  display: none;
}

.case-figure,
.case-step .img-slot {
  margin-inline: calc(-1 * var(--pad-x));
}

/* a figure that is drawn rather than photographed keeps the text column: a
   screenshot earns the extra width, a diagram set in the page's own type
   only loses its alignment with the prose it belongs to. Its caption has no
   bleed left to pad back out of, so that cancels too. */
.case-figure--inset {
  margin-inline: 0;
}

.case-figure--inset figcaption {
  padding-inline: 0;
}

.case-panel .img-slot {
  border-inline-width: 0;
}

/* screenshots and screen recordings are shown whole: cropping a UI to
   fill a box loses the very thing the shot is there to show */
.img-slot img,
.img-slot video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--panel);
}

/* the hero: the product, one screen at a time. The band keeps its height
   when you switch, so the page never jumps; the phone is centred in it.
   Marks are placed in image coordinates (--x / --y) and hold their spot at
   every width. On a fine pointer the red dot carries the note; keyboard and
   touch get the same words as a pill. Without JS both screens stack. */
.case-shot .shot {
  position: relative;
  display: grid;
  /* the row is the band, never the image: a tall phone must fit, not stretch */
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  /* 16/9 is a desktop case's band: its widest shot fills it. A case whose
     shots are all portrait (a phone app) declares a taller band instead, or
     every frame in it would stand as a stamp between two grey margins. */
  aspect-ratio: var(--band-ratio, 16 / 9);
  overflow: hidden;
  background: var(--panel);
  border-block: var(--hair) solid var(--ink);
}

html:not(.js) .case-shot .shot {
  display: block;
  aspect-ratio: auto;
}

/* the frame shrink-wraps its image, so the marks sit in image coordinates */
.shot-frame {
  position: relative;
  grid-area: 1 / 1;
  line-height: 0;
}

/* a wide shot stands at the band's full height and takes the width its own
   ratio asks for (--shot-ratio, the way an .img-slot declares one). A shot
   that is not the band's 16/9 therefore sits narrower instead of being
   cropped or stretched, and the frame still measures exactly the image, so
   the marks keep their coordinates. contain is the belt and braces: an
   ultra-wide shot clamped by max-width letterboxes rather than distorts. */
.shot-frame:not(.shot-frame--phone) {
  aspect-ratio: var(--shot-ratio, 16 / 9);
  height: 100%;
  width: auto;
  max-width: 100%;
}

.shot-frame:not(.shot-frame--phone) img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* the phone stands in the band at its own scale, framed like a screen. The
   air around it is the band's padding, so the frame stays exactly the image
   and the marks keep their coordinates. */
html.js .shot.is-phone {
  padding-block: var(--space-4);
}

.shot-frame--phone {
  align-self: stretch;
}

.shot-frame--phone img {
  width: auto;
  height: 100%;
  border: var(--hair) solid var(--ink);
}

/* the marks leave no trace on the shot: they are just the areas that have
   something to say. Cross one and the pointer says it, over a faint red
   wash that shows how far the note reaches. */
.shot-mark {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  height: var(--h);
  border: 0;
  background: none;
}

.shot-mark:focus-visible {
  outline: none;
}

.shot-mark:hover,
.shot-mark:focus-visible {
  background: var(--accent-wash);
}

/* the dot's stand-in, for pointers that have no dot: same pill, same red */
.shot-tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 7px);
  transform: translateX(-50%);
  padding: var(--space-1) var(--space-2);
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
}

/* marks near the right edge hang their pill inward, or it would be clipped */
.shot-mark--end .shot-tip {
  left: auto;
  right: -2px;
  transform: none;
}

html:not(.has-dot) .shot-mark:hover .shot-tip,
.shot-mark:focus-visible .shot-tip {
  opacity: 1;
}

/* the switch sits on the band's top rule, aligned with the text column */
.shot-switch {
  display: none; /* switching screens is a JS feature, like the tag row */
}

html.js .shot-switch {
  display: flex;
  width: max-content;
  margin-left: var(--pad-x);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
  border-bottom-width: 0;
}

.shot-switch button {
  padding: var(--space-1) var(--space-3);
  background: var(--paper);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

.shot-switch button:hover,
.shot-switch button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

/* two shots side by side, sharing one hairline: either two equal screens or
   a wide one with a phone beside it (.img-slot--phone sizes that second cell) */
.case-figure--pair {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: start;
  gap: var(--hair);
  background: var(--ink);
  border-block: var(--hair) solid var(--ink);
}

.case-figure--pair:has(.img-slot--phone) {
  grid-template-columns: 1fr auto;
}

.case-figure--pair .img-slot {
  border: 0;
}

.case-figure--pair figcaption {
  grid-column: 1 / -1;
  background: var(--panel);
  padding-block: var(--space-2);
}

.img-slot--phone {
  width: clamp(120px, 20vw, 180px);
}

/* a band of phone screens, in the hero shot's language: a panel strip with
   air, each phone capped at hand size and framed by its own hairline. Never
   full column width: a phone is a column, not a banner. Phones wrap into a
   stack when the band gets narrow. */
.case-figure--phones {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-4) var(--pad-x);
  background: var(--panel);
  border-block: var(--hair) solid var(--ink);
}

/* --phone-w is the hand size: a band of two takes the default, a band of
   three sets it narrower so the set reads as one row instead of a pair and
   an orphan. Shrink still applies, so a narrow column wraps as before. */
.case-figure--phones .img-slot {
  flex: 0 1 var(--phone-w, 300px);
  margin-inline: 0; /* undo the band bleed .case-step slots inherit */
  border: var(--hair) solid var(--ink);
}

.case-figure--phones figcaption {
  flex-basis: 100%;
  padding-top: var(--space-2);
  text-align: center;
}

/* text with a small looping figure at its side: the media illustrates, it
   does not interrupt the reading. On narrow screens it drops below. */
.case-aside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(160px, 22vw, 230px);
  gap: var(--space-5);
  align-items: start;
}

.case-aside .aside-media {
  display: grid;
  gap: var(--space-3);
}

.case-aside .img-slot {
  margin-inline: 0;
  border: var(--hair) solid var(--ink);
}

.case-aside figcaption {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--gray-3);
}

@media (max-width: 700px) {
  .case-aside {
    grid-template-columns: minmax(0, 1fr);
  }

  .case-aside .aside-media {
    justify-items: center;
  }

  .case-aside .img-slot {
    width: min(230px, 100%);
  }
}

.slot-label {
  padding: var(--space-1) var(--space-2);
  background: var(--panel);
  border: var(--hair) solid var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-align: center;
  color: var(--gray-3);
}

/* the figure bleeds, the caption stays with the text column */
.case-figure figcaption {
  margin-top: var(--space-2);
  padding-inline: var(--pad-x);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--gray-3);
}

/* a link out of the prose, in the page's own polarity: ink and underlined
   while it reads as text, inverted the moment it is the thing under the
   pointer. The global rule only inherits colour, so without this a live
   project link would arrive as browser blue on a monochrome sheet. */
.case-link {
  text-decoration-thickness: from-font;
  text-underline-offset: 2px;
}

.case-link:hover,
.case-link:focus-visible {
  background: var(--ink);
  color: var(--paper);
  text-decoration: none;
  outline: none;
}

.case-next {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3);
}

.case-next a {
  font-family: var(--font-mono);
  font-size: var(--text-s);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  padding: var(--space-2);
}

.case-next a:hover {
  background: var(--ink);
  color: var(--paper);
}

/* 6f. page-enter: a mechanical dither-in, not a fade. Hidden state lives
   ONLY inside the keyframes so `animation: none` yields visible content. */
@keyframes boot {
  from {
    opacity: 0;
  }
}

html.js body:not(.page-index) main > *,
html.js body:not(.page-index) .head-strip {
  animation: boot 200ms steps(2, end) both;
}

/* cross-document transition: progressive enhancement, Chromium only */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

/* 6h. pointer + sound (js/interactions.js) ------------------------------- */

/* the red dot replaces the arrow; the native cursor only comes back where
   text is selected, so reading and copying still feel normal */
html.has-dot,
html.has-dot a,
html.has-dot button,
html.has-dot canvas {
  cursor: none;
}

.cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  display: grid;
  place-items: center; /* the label centres, so the pill grows both ways */
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
  overflow: hidden; /* the word is clipped until the pill has grown round it */
  transition: width 240ms cubic-bezier(0.2, 0.9, 0.2, 1),
    height 240ms cubic-bezier(0.2, 0.9, 0.2, 1),
    opacity 120ms linear;
  will-change: transform;
}

.cursor-dot.is-live {
  opacity: 1;
}

/* over anything clickable the dot opens into a ring: the site's polarity
   trick, in the one accent colour */
.cursor-dot.is-link {
  width: 30px;
  height: 30px;
  background: none;
  border: 2px solid var(--accent);
}

.cursor-dot.is-down {
  width: 10px;
  height: 10px;
}

/* over something that names itself (a hero scene, an art page spot), the dot
   grows into the word: the pill opens out of the dot around the pointer and
   the label rises inside it. No icon to decode, and the one accent colour.
   --label-w / --label-h are measured in js/interactions.js: the pill cannot
   ease towards `auto`, so it eases towards the label's own box. */
.cursor-dot.has-label {
  width: var(--label-w, auto);
  height: var(--label-h, auto);
  background: var(--accent);
  border: 0;
}

.cursor-label {
  padding: var(--space-1) var(--space-2);
  color: #fff;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  line-height: 1;
  white-space: nowrap; /* its natural width is what the pill grows to */
  opacity: 0;
  transform: scale(0.92);
  transition: opacity 130ms linear,
    transform 240ms cubic-bezier(0.2, 0.9, 0.2, 1);
}

/* the word waits for the pill to be on its way before it arrives; leaving,
   it goes at once, so the pill never shrinks around visible text */
.cursor-dot.has-label .cursor-label {
  opacity: 1;
  transform: none;
  transition-delay: 70ms;
}

.cursor-dot.is-link.is-down {
  width: 22px;
  height: 22px;
}

/* the switches (sound, motion): icons, nothing else, pinned to the
   bottom-left of the wordmark cell. A hairline box marks each as a control;
   the hit area is larger than the mark. Accent means the visitor has moved
   the switch off its default - sound on, or motion stopped. */
.cell-switches {
  display: flex;
  flex-wrap: wrap; /* a narrow cell stacks them rather than squashing them */
  gap: var(--space-1);
  align-self: flex-start;
  margin-top: auto; /* the cell is a column: this drops it to the floor */
}

.cell-switch {
  display: inline-flex;
  flex: none; /* the box is the hit area: it never shrinks to fit */
  align-items: center;
  padding: var(--space-2);
  background: var(--panel);
  border: var(--hair) solid var(--gray-2);
  pointer-events: auto;
}

.cell-switch:hover,
.cell-switch:focus-visible {
  border-color: var(--ink);
}

.cell-switch[aria-pressed="true"] {
  border-color: var(--accent);
}

.switch-icon {
  display: block;
  color: var(--gray-2);
}

.cell-switch:hover .switch-icon {
  color: var(--ink);
}

.cell-switch[aria-pressed="true"] .switch-icon {
  color: var(--accent);
}

/* a speaker: crossed out when muted, waves when playing */
.si-body,
.mi-pause,
.mi-play {
  fill: currentColor;
}

.si-wave,
.si-mute {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
}

.sound-toggle .si-wave {
  display: none; /* muted by default: no waves */
}

.sound-toggle[aria-pressed="true"] .si-mute {
  display: none;
}

.sound-toggle[aria-pressed="true"] .si-wave {
  display: block;
}

/* the outer wave breathes, so "on" is legible at a glance */
.sound-toggle[aria-pressed="true"] .si-wave-2 {
  animation: sound-wave 1.6s ease-in-out infinite;
}

@keyframes sound-wave {
  50% {
    opacity: 0.25;
  }
}

/* the nudge: a hairline note beside the speaker, only while the switch says
   "on" and the browser is still holding the audio back. It waits for the
   first click, then leaves for good, so it is never chrome the visitor has
   to dismiss. Built in js/interactions.js. */
.switch-nudge {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: var(--space-1) var(--space-2);
  background: var(--panel);
  border: var(--hair) solid var(--accent);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none; /* a label, not a third control */
  animation: switch-nudge-in 240ms cubic-bezier(0.2, 0.9, 0.2, 1) both;
}

@keyframes switch-nudge-in {
  from {
    opacity: 0;
    transform: translateX(-4px);
  }
}

/* dismissed: it fades where it stands, then leaves the DOM */
.switch-nudge.is-gone {
  opacity: 0;
  animation: none;
  transition: opacity 200ms linear;
}

/* the motion switch shows where it stands, like the speaker: a play mark
   while the artwork runs, bars once it is frozen */
.motion-toggle .mi-play {
  display: none;
}

.motion-toggle[aria-pressed="true"] .mi-pause {
  display: none;
}

.motion-toggle[aria-pressed="true"] .mi-play {
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .sound-toggle[aria-pressed="true"] .si-wave-2 {
    animation: none;
  }

  /* the note still fades, it just does not slide in */
  @keyframes switch-nudge-in {
    from {
      opacity: 0;
    }
  }
}

/* 6i. process case study: diagrams drawn in the page's own language ------
   All four blocks are CSS only, so they stay crisp at any size and need no
   asset. They exist to make a process scannable without reading every word. */

/* the reframing, as polarity: the old question struck out on paper, the
   new one set in ink */
.shift {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.shift p {
  margin: 0;
  background: var(--panel);
  padding: var(--space-3);
  font-size: var(--text-m);
}

.shift .label {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--gray-3);
}

/* the p selectors outweigh the reading-copy colour set on .case-panel p */
.shift p.shift-from {
  color: var(--gray-3);
  text-decoration: line-through;
  text-decoration-thickness: var(--hair);
}

.shift-from .label {
  text-decoration: none;
}

.shift p.shift-to {
  background: var(--ink) !important;
  color: var(--paper);
}

.shift-to .label {
  color: var(--gray-2);
}

/* the two week rhythm: a lattice of days, filled where the work happens */
.cadence {
  display: grid;
  grid-template-columns: 5.5rem repeat(5, 1fr);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track);
  text-transform: uppercase;
}

.cadence-head,
.cadence-row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  gap: var(--hair);
}

.cadence-head > span,
.cadence-week,
.cadence-cell {
  background: var(--panel);
  padding: var(--space-2);
  min-height: 2.6rem;
}

.cadence-head > span {
  color: var(--gray-3);
  min-height: 0;
  padding-block: var(--space-1);
}

.cadence-week {
  display: flex;
  align-items: center;
  letter-spacing: var(--track-wide);
}

.cadence-cell {
  display: flex;
  align-items: center;
}

.cadence-cell em {
  font-style: normal;
}

/* sessions run: solid ink. Processing: the engine's own hatch, so the two
   halves of the cycle read as different kinds of work */
.cadence-cell.is-on {
  background: var(--ink);
  color: var(--paper);
}

.cadence-cell.is-soft {
  background:
    repeating-linear-gradient(45deg, var(--gray-1) 0 2px, var(--panel) 2px 6px);
}

/* the loop: six cells and a bar that sends you back to the first */
.loop {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.loop-step {
  background: var(--panel);
  padding: var(--space-3);
}

.loop-step h3 {
  font-family: var(--font-mono);
  font-size: var(--text-s);
}

.loop-step p {
  margin-top: var(--space-2);
  font-size: var(--text-s);
  color: var(--gray-3);
  max-width: none;
}

.loop-return {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

.loop-return::before {
  content: "";
  width: 1.5rem;
  height: 0;
  border-top: 2px solid var(--paper);
}

/* the four standing risks */
.themes {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--hair);
  background: var(--ink);
  border: var(--hair) solid var(--ink);
}

.themes li {
  background: var(--panel);
  padding: var(--space-3);
  font-size: var(--text-s);
}

.themes .idx {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--ink);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
}

/* a worked example, and the one line only Filip can fill */
.example {
  border: var(--hair) solid var(--ink);
  padding: var(--space-4);
}

.example .label {
  color: var(--gray-3);
}

.example p {
  margin-top: var(--space-3);
}

.example-todo {
  border: var(--hair) dashed var(--gray-2);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--gray-3);
}

@media (max-width: 700px) {
  .shift,
  .loop,
  .themes {
    grid-template-columns: 1fr;
  }

  .cadence {
    grid-template-columns: 4.5rem repeat(5, 1fr);
    font-size: 0.5rem;
  }

  .cadence-cell em {
    display: none; /* the prose carries the detail at this size */
  }
}

/* 7. media + preferences + fallbacks (keep last) -------------------------- */
@media (min-width: 901px) {
  .case-facts {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* below ~1240px the index cell would crowd the reading column, so it
   spans the foot of the viewport as one flush band instead */
/* unplaced index spans the viewport foot: keep the last panel clear of it */
@media (max-width: 1239px) {
  .case-doc {
    padding-bottom: calc(var(--space-6) + 52px);
  }
}

@media (max-width: 700px) {
  .case-figure--pair,
  .case-figure--pair:has(.img-slot--phone) {
    grid-template-columns: 1fr;
    justify-items: center;
  }
}

/* narrow type + spacing (the flush-row fallback itself is width-agnostic) */
@media (max-width: 760px) {
  [data-cell="menu"] a {
    padding: var(--space-2) var(--space-1);
    letter-spacing: var(--track);
  }

  /* the fallback menu cell reads like the artwork pages' menu on a phone:
     links stacked down its right edge, from the top of the cell (the base
     rule's justify-content means bottom once the axis turns vertical). The
     gap outweighs the links' negative hit-area margins, or the column
     collapses onto itself. */
  body.tiles-ready [data-cell="menu"]:not(.is-placed) {
    flex-direction: column;
    align-items: flex-end;
    justify-content: flex-start;
    gap: var(--space-4);
  }

  .case-hero {
    padding-block: var(--space-5);
  }
}

@media (max-width: 900px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 600px) {
  /* keep the bleed exact: the variable is what figures cancel */
  .case-panel {
    --pad-x: var(--space-3);
    padding: var(--space-4) var(--pad-x);
  }

  .case-doc {
    gap: var(--space-4);
  }

  .case-facts,
  .case-metrics {
    grid-template-columns: 1fr;
  }

  /* on a phone a fixed band would shrink the phone shot to a stamp: let each
     view take the height it needs instead */
  .case-shot .shot {
    aspect-ratio: auto;
    grid-template-rows: auto;
  }

  html.js .shot.is-phone {
    padding-block: var(--space-3);
  }

  .shot-frame--phone img {
    width: min(62vw, 240px);
    height: auto;
  }

  /* space-between pins the ends of a line, so a nav that wraps on a phone
     staggers left, right, left. Narrow, the links just read down the margin. */
  .case-next {
    justify-content: flex-start;
  }

  /* a stacked phone is alone on its line, so a band that narrowed its phones
     to fit three across has nothing left to fit: every band stacks at the
     same hand size */
  .case-figure--phones .img-slot {
    flex-basis: 300px;
  }

  .case-sub {
    font-size: var(--text-m);
  }

  .card-grid {
    grid-template-columns: 1fr;
  }

  .grid-cell-overlay {
    padding: var(--space-2);
  }

  .grid-cell-overlay .wordmark,
  .case-cell .wordmark {
    font-size: var(--text-s);
  }

  /* on a phone the menu cell is one lattice column wide: the links read
     down its right edge instead of wrapping mid-label. The gap has to
     outweigh the links' negative hit-area margins (-space-2 vertical), or
     the column collapses onto itself. */
  .grid-cell-overlay[data-cell="menu"] nav {
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-4);
  }

  /* the fun-facts control row has to fit a narrow panel: tighter everything */
  .cell-bio .bio-body {
    padding: var(--space-3);
  }

  .ff-step {
    padding: var(--space-2);
  }

  .ff-tick {
    width: 18px;
  }

  .tile-link {
    font-size: 1rem;
  }

  .site-head {
    padding-inline: var(--space-3);
  }

  .site-head .wordmark {
    font-size: var(--text-xs);
  }

  .site-head nav a {
    padding: 0 var(--space-2);
  }
}

/* the drawer is only as wide as the bio above it, so on anything but a big
   screen the image slot would eat the sentence: spend the width on the fact */
@media (max-width: 1499px) {
  html.js .cell-funfact .ff-shot {
    display: none;
  }
}

/* short landscape phones: allow the landing to scroll rather than crush */
@media (max-height: 420px) {
  .page-index {
    overflow: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}

/* no-JS / engine-failed: static column, fully usable, intentional */
html:not(.js) .grid-host,
html.engine-failed .grid-host,
html:not(.js) .hover-img,
html.engine-failed .hover-img {
  display: none;
}

html:not(.js) .stage,
html.engine-failed .stage {
  position: static;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4);
}

html:not(.js) .grid-cell-overlay,
html.engine-failed .grid-cell-overlay {
  position: static;
  visibility: visible;
  padding: 0;
}

html:not(.js) .tile-nav,
html.engine-failed .tile-nav {
  position: static;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  pointer-events: auto;
}

html:not(.js) .tile-link,
html.engine-failed .tile-link {
  position: static;
  visibility: visible;
  width: min(420px, 84vw);
  min-height: 96px;
  background: var(--ink);
  color: var(--paper);
}

html:not(.js) .tile-link:hover,
html.engine-failed .tile-link:hover,
html:not(.js) .tile-link:focus-visible,
html.engine-failed .tile-link:focus-visible {
  outline: var(--hair) solid var(--ink);
  outline-offset: 2px;
}

html:not(.js) .thumb,
html.engine-failed .thumb {
  background: var(--paper);
}

/* work page without JS: plain readable list, no preview pane */
html:not(.js) .cell-preview,
html.engine-failed .cell-preview {
  display: none;
}

html:not(.js) .cell-list,
html.engine-failed .cell-list {
  border: var(--hair) solid var(--ink);
  width: min(760px, 100%);
}

/* about page without JS: readable bio, no canvas-dependent photo cells */
html:not(.js) .photo-overlay,
html.engine-failed .photo-overlay {
  display: none;
}

html:not(.js) .cell-bio,
html.engine-failed .cell-bio {
  border: var(--hair) solid var(--ink);
  width: min(760px, 100%);
}

html:not(.js) .cell-bio .bio-body,
html.engine-failed .cell-bio .bio-body {
  min-height: auto;
  max-width: 62ch;
}

html:not(.js) .cell-bio .bio-colophon,
html.engine-failed .cell-bio .bio-colophon {
  margin-top: var(--space-4);
}

/* the fun-facts header only toggles something when JS is there to toggle it */
html:not(.js) .ff-toggle {
  cursor: default;
  pointer-events: none;
}

html:not(.js) .w-row > *,
html.engine-failed .w-row > * {
  mix-blend-mode: normal;
  color: var(--ink);
}
