/* =========================================================================
   onepager.css - the base layer of korent.pages.dev.  Direction: PINACOTECA.

   The page is a wall in a small, badly lit, very good private collection.
   The ground is painted (three oklab colour fields + a fractal-noise tile),
   not filled. Headings are marginalia in the left column, never bars with a
   hairline under them. Eyebrow count for the page: 0.

   No style="" attribute, no inline <style>, no third party: the CSP has
   neither 'unsafe-inline' nor 'unsafe-hashes'. Per-item stagger, jitter and
   tilt therefore live in the :nth-child table near the bottom of this file,
   NOT in the generator.

   Loaded in BOTH modes; the XP desktop chrome lives in styles/main.css et al.
   ========================================================================= */

/* ---- RESET ------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
body, h1, h2, h3, p, ul, ol, dl, dd, figure { margin: 0; }
ul[role="list"], ol[role="list"] { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
button, input { font: inherit; color: inherit; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  font-synthesis: none;              /* no faux bold, no faux italic, ever  */
  /* scroll-behavior is deliberately NOT set: round 1's global smooth scroll
     hijacked every anchor jump and fought the user's own scroll. */
}

body {
  margin: 0;
  /* The portrait deliberately overhangs its column into the right margin.
     clip (not hidden) guards against a horizontal scrollbar at the exact
     width where the container stops growing, and does not create a scroll
     container, so position: sticky keeps working. */
  overflow-x: clip;
  background-color: var(--ground);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-0);
  font-weight: var(--wt-regular);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-0);
  -webkit-font-smoothing: antialiased;
}

/* =========================================================================
   THE PAINTED SURFACE.  Two fixed composited layers, painted once, never
   repainted on scroll, 0 HTTP requests, 0 KB of image.
   ========================================================================= */

/* (a) three layered colour fields. Because the layer is fixed and the content
   moves across it, the visible background is never the same mix twice. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background-color: var(--ground);
  background-image:
    radial-gradient(115% 80% at 14% -12%,
      color-mix(in oklab, var(--wash-a) 62%, transparent) 0%, transparent 58%),
    radial-gradient(85% 65% at calc(78% + var(--px, 0px)) calc(30% + var(--py, 0px)),
      color-mix(in oklab, var(--wash-b) 48%, transparent) 0%, transparent 56%),
    radial-gradient(130% 70% at 46% 116%,
      color-mix(in oklab, var(--wash-c) 44%, transparent) 0%, transparent 62%);
}

/* (b) grain over everything, so page and photograph share one surface. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-size: 180px 180px;
}

/* (c) bands arrive and leave instead of butting. No section edge on this
   page is ever a straight line: a band is laid on with a flat brush, so its
   top and bottom are a dragged, pooled wash edge (Images/paint/band-edge.webp,
   1.9 KB, alpha only; the colour is still the band token, so every contrast
   pair in tokens.css is unchanged). Top half and bottom half are two layers,
   the bottom one flipped, so a band of any height keeps an edge of one size.
   The strip tiles along x and each edge starts it somewhere else. */
.band { position: relative; --edge-h: clamp(4.5rem, 3rem + 5vw, 8rem); --edge-x: 0rem; }
.band::before,
.band::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--band-bg);
  mask-image: url("/Images/paint/band-edge.webp"), linear-gradient(#000 0 0);
  mask-size: 60rem var(--edge-h), 100% 100%;
  mask-position: var(--edge-x) 0, 0 calc(var(--edge-h) - 1px);
  mask-repeat: repeat-x, no-repeat;
}
.band::before { inset-block: -5rem 50%; }
.band::after  { inset-block: calc(50% - 1px) -5rem; transform: scaleY(-1); --edge-x: 23rem; }
.section--resources { --band-bg: var(--band-cv); }
.section--contact { --band-bg: var(--band-deep); --edge-x: 37rem; }
.section--contact.band::after { --edge-x: 11rem; }

/* (d) Pigment. Real washes laid on the wall where the page turns: behind the
   hero, spilling down into Right now, and across the Work to Projects seam.
   The masks are alpha-only WebP baked once from a watercolour model (warped
   blobs, two overlapping glazes, a pooled rim where the pigment dried, a
   mottled body, a faint bleed past the edge): wash-warm 7.9 KB, wash-cool
   6.7 KB. Colour, opacity and blend are tokens, so one file serves both
   themes: in daylight the wash multiplies into the paper like a glaze, at
   night it sits on the wall. Each pseudo-element is sized to its mask's own
   16:9 box, so the paint is never cut by a box edge inside the page, only by
   the viewport, which is where a canvas ends anyway (body clips x).
   Static: nothing here animates, so reduced motion changes nothing. z-index
   -1 in the root stacking context puts it over the ground, under the grain
   and under every word. Opacities are the measured AA limits (tokens.css). */
/* overflow-x: clip, not hidden: the paint may bleed up and down into the
   neighbouring sections, but a wash wider than the viewport must never widen
   the page (a phone zooms out to fit content, whatever body clips). */
.hero, .section--projects { position: relative; overflow-x: clip; }
.hero::before,
.hero::after,
.section--projects::before {
  content: "";
  position: absolute;
  z-index: -1;
  aspect-ratio: 16 / 9;
  pointer-events: none;
  mix-blend-mode: var(--pig-blend);
  mask-size: 100% 100%;
  mask-repeat: no-repeat;
}
.hero::before {                          /* warm: behind the facts, leaning on the name */
  inset-block-start: calc(var(--header-h) * -1);
  inset-inline-end: -14%;
  inline-size: max(62rem, 90%);
  background: var(--pig-warm);
  opacity: var(--pig-warm-a);
  mask-image: url("/Images/paint/wash-warm.webp");
}
.hero::after {                           /* cool: low left, bleeding into Right now */
  inset-block-end: -18rem;
  inset-inline-start: -18%;
  inline-size: max(56rem, 80%);
  background: var(--pig-cool);
  opacity: var(--pig-cool-a);
  mask-image: url("/Images/paint/wash-cool.webp");
}
.section--projects::before {             /* the seam: the warm file, mirrored, in viridian */
  inset-block-start: -15rem;
  inset-inline-start: -10%;
  inline-size: max(54rem, 72%);
  background: var(--pig-deep);
  opacity: var(--pig-deep-a);
  mask-image: url("/Images/paint/wash-warm.webp");
  transform: scaleX(-1);
}
@media (max-width: 639px) {
  .hero::before { inline-size: 210%; inset-inline-end: -62%; inset-block-start: -1rem; }
  .hero::after  { inline-size: 230%; inset-inline-start: -72%; inset-block-end: -10rem; }
  .section--projects::before { inline-size: 220%; inset-inline-start: -40%; inset-block-start: -8rem; }
}

/* (e) The brush. Every separator on the page is one pass of a loaded brush,
   not a hairline: a tapered 240x12 path with dry bristle gaps, about 0.45 KB
   as a data URI. Shape from the mask, colour from --stroke, and no two uses
   start in the same place: the rules below move, flip and shorten it. */
.rail__past::before,
.index::before,
.rows::before,
.site-footer::before {
  content: "";
  position: absolute;
  inset-block-start: 0;               /* inside the list's own top padding: never touches the line above */
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 7px;
  pointer-events: none;
  background: var(--stroke);
  mask: var(--brush-h) 0 0 / 100% 100% no-repeat;
}
.index::before       { mask-image: var(--brush-h2); inline-size: 96%; }
.rail__past::before  { mask-image: var(--brush-h2); transform: scaleX(-1); }
.lull:nth-child(odd) .rows::before { transform: scaleX(-1); inline-size: 93%; }
.site-footer::before {
  inset-inline-start: max(var(--gutter), calc((100% - var(--wrap-max)) / 2 + var(--gutter)));
  inline-size: min(calc(100% - var(--gutter) * 2), calc(var(--wrap-max) - var(--gutter) * 2));
}

/* Links are underlined with the same brush: a stroke of sienna under the
   word, one per line (box-decoration-break: clone), fuller on hover. Its
   colour has to be baked into the URI, so tokens.css holds a light and a dark
   copy. Forced-colours mode drops background images; the real underline comes
   back there. */
.link-underline,
.feature__link,
.index__t,
.contact__links a,
.site-footer a,
.site-footer button {
  text-decoration: none;
  background-image: var(--brush-u);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.3em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.index li:nth-child(even) .index__t,
.contact__links li:nth-child(even) a,
.site-footer li:nth-child(even) a { background-image: var(--brush-u2); }
.index li:nth-child(3n) .index__t { background-size: 93% 0.3em; }
/* Flex links are taller than their text (44px targets): hang the stroke just
   under the glyphs instead of at the bottom of the hit area. */
.contact__links a { background-position: 0 calc(50% + 0.66em); }
@media (hover: hover) and (pointer: fine) {
  .link-underline:hover,
  .feature:hover .feature__link,
  .index li:hover .index__t,
  .contact__links a:hover,
  .site-footer a:hover,
  .site-footer button:hover { background-size: 100% 0.42em; }
}
@media (forced-colors: active) {
  .link-underline, .feature__link, .index__t,
  .contact__links a, .site-footer a, .site-footer button { text-decoration: underline; }
}

/* --ink-faint computes to 4.41:1 on --band-deep in light, under AA. On any
   band the quietest permitted text step is --ink-quiet. */
.band :where(.faint) { color: var(--ink-quiet); }

::selection { background: color-mix(in oklab, var(--accent) 28%, transparent); color: var(--ink-strong); }

/* Focus is instant and universal; no rule anywhere suppresses a focus ring. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: inherit;
}

/* ---- TYPE ROLES --------------------------------------------------------- */
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }

h1 {
  font-family: var(--font-display);
  font-size: var(--t-5);
  font-weight: var(--wt-medium);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-5);
  color: var(--ink-strong);
  /* Newsreader's descender is 26.5% of the em and "Maj Korent" has a j.
     At leading 1.00 it would clip into the next element without this. */
  padding-block-end: 0.06em;
}

h3 {
  font-family: var(--font-display);
  font-size: var(--t-1);
  font-weight: var(--wt-medium);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-1);
  color: var(--ink-strong);
}

a { color: inherit; }
.link-underline {
  color: var(--accent);
  transition: background-size var(--d-hover) var(--ease-out);
}

.u-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Every date, size and year on the page. Instrument Sans has real tnum,
   which is what lets this page have no monospace role at all. */
.num { font-variant-numeric: tabular-nums lining-nums; }

/* 404.html promotes its one heading to h1 for document outline (a11y heading
   order), but it must still read at section size, not at hero size. */
.nf__code { font-size: var(--t--2); letter-spacing: var(--tr--2); color: var(--ink-quiet); font-variant-numeric: tabular-nums lining-nums; }
.nf__h { font-size: var(--t-3); letter-spacing: var(--tr-3); line-height: var(--lh-tight); }
.nf__lead { margin-block-start: var(--space-m); max-inline-size: var(--measure); }

/* ---- SKIP LINK ---------------------------------------------------------- */
.skip {
  position: absolute;
  inset-block-start: var(--space-xs);
  inset-inline-start: var(--space-xs);
  z-index: 20;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-xs) var(--space-m);
  background: var(--raised);
  color: var(--ink-strong);
  border: 1px solid var(--border-ctrl);
  border-radius: var(--radius-pill);
  font-size: var(--t--1);
  font-weight: var(--wt-medium);
  text-decoration: none;
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

/* ---- BUTTONS ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-s) var(--space-l);
  border: 1px solid transparent;
  border-radius: var(--radius-xs);
  font-size: var(--t-0);
  font-weight: var(--wt-medium);
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--d-press) var(--ease-out),
              background-color var(--d-hover) ease;
}
.btn--solid { background: var(--btn-solid-bg); color: var(--btn-solid-fg); }
.btn:active { transform: scale(0.97); }

/* ---- HEADER -------------------------------------------------------------
   A masked gradient where content meets floating chrome, not the internet's
   most common 1px rule. Reads as a material, not as a divider. */
.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 10;
  height: var(--header-h);
  background: color-mix(in oklab, var(--ground) 72%, transparent);
  backdrop-filter: blur(14px) saturate(118%);
  -webkit-backdrop-filter: blur(14px) saturate(118%);
  border: 0;
}
.site-header::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 100%;
  block-size: 1.5rem;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--ground), transparent);
  opacity: 0.72;
}
@media (prefers-reduced-transparency: reduce) {
  .site-header { background: var(--ground); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  height: 100%;
}
.site-header__mark {
  font-family: var(--font-display);
  font-size: var(--t--1);
  font-weight: var(--wt-semi);
  letter-spacing: 0.06em;
  color: var(--ink-strong);
  text-decoration: none;
  padding-block: var(--space-xs);
}
.site-nav ul { display: flex; gap: var(--space-l); margin: 0; }
.site-nav a {
  display: inline-block;
  padding-block: var(--space-xs);
  font-size: var(--t--2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
  text-decoration: none;
  transition: color var(--d-hover) ease;
}
@media (hover: hover) and (pointer: fine) {
  .site-nav a:hover { color: var(--ink-strong); }
}
@media (max-width: 639px) { .site-nav { display: none; } }
@media (pointer: coarse) {
  .site-nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ---- THE PERSISTENT MODE SWITCH ---------------------------------------- */
.mode-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: 32px;
  padding: var(--space-2xs) var(--space-s);
  font-size: var(--t--2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--d-hover) ease,
              border-color var(--d-hover) ease,
              transform var(--d-press) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .mode-switch:hover { color: var(--ink-strong); border-color: var(--border-ctrl); }
}
.mode-switch:active { transform: scale(0.97); }
.mode-switch__glyph { transform: translateY(-0.5px); }
@media (pointer: coarse) { .mode-switch { min-height: 44px; padding-block: var(--space-xs); } }
/* Below 640 the section nav is gone, so the switch is the only control in the
   header and it is the way to desktop mode on a phone: full 44px whatever the
   pointer reports (a narrow desktop window, a stylus, a mis-reporting tablet). */
@media (max-width: 639px) { .site-header .mode-switch { min-height: 44px; padding-inline: var(--space-m); } }

.mode-escape {
  position: fixed;
  inset-block-start: var(--space-xs);
  inset-inline-end: var(--space-xs);
  z-index: 99999;
  background: var(--ground);
}
:root:not([data-mode="desktop"]) .mode-escape { display: none; }

/* In desktop mode the pill sits over the XP wallpaper, not the painted
   ground: the dark rounded one-pager pill reads as a foreign, half-broken
   control on Luna chrome. Re-skin it as a small Luna push button instead.
   Values are literal, not var(--xp-*): xp-theme.css loads lazily, only
   after desktop-loader.js runs, so this button (rendered on first paint,
   inside index.html) cannot depend on its custom properties existing yet. */
:root[data-mode="desktop"] .mode-switch.mode-escape {
  inset-block-start: 8px;
  inset-inline-end: 8px;
  min-height: 23px;
  padding: 3px 10px 3px 8px;
  gap: 5px;
  font: 11px/1.2 Tahoma, "Segoe UI", Verdana, "DejaVu Sans", sans-serif;
  font-weight: 400;
  letter-spacing: 0;
  color: #1b1b1b;
  background: linear-gradient(180deg, #ffffff 0%, #f4f3ee 55%, #e3e0d2 100%);
  border: 1px solid #003c74;
  border-radius: 3px;
  box-shadow: inset 1px 1px 0 #fff, inset -1px -1px 0 rgb(0 0 0 / 0.07);
}
:root[data-mode="desktop"] .mode-switch.mode-escape:hover {
  color: #1b1b1b;
  border-color: #003c74;
}
:root[data-mode="desktop"] .mode-switch.mode-escape:focus-visible {
  outline: 2px solid #0a5ae6;
  outline-offset: 2px;
}
/* Icons run in fixed-width columns from the left edge (styles/main.css
   .desktop-icons); at narrow widths enough columns are needed that the
   last one reaches close to this top-right corner. Collapsing the label to
   an icon-only button (font-size: 0 zeroes the text run's rendered width;
   the SVG glyph has explicit width/height attributes so it is unaffected,
   and the text stays in the DOM so the accessible name is unchanged) keeps
   the whole control narrow enough to clear the icon grid at these widths. */
@media (max-width: 480px) {
  :root[data-mode="desktop"] .mode-switch.mode-escape {
    font-size: 0;
    gap: 0;
    padding-inline: 6px;
  }
}
@media (pointer: coarse) {
  :root[data-mode="desktop"] .mode-switch.mode-escape { min-width: 44px; min-height: 44px; }
}

/* =========================================================================
   SECTION SHELL.  Vertical rhythm varies per section on purpose: that is the
   other half of the fix for "nine sections, one opening gesture".
   ========================================================================= */
main > section { padding-block: var(--section-y); }
section[id] { scroll-margin-top: calc(var(--header-h) + var(--space-l)); }

.section > .wrap { align-items: baseline; }

/* The heading is marginalia: a small quiet word hanging in the left margin,
   baseline-aligned to the first line of prose. Sentence case, no tracking
   theatre, no hairline. The hairline is gone on purpose. */
main h2 {
  font-family: var(--font-text);
  font-size: var(--t--2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--2);
  line-height: var(--lh-meta);
  color: var(--ink-quiet);
  border: 0;
  margin: 0;
}
@media (max-width: 1023px) {
  main h2 { margin-block-end: var(--space-m); }
}

.prose p + p { margin-block-start: var(--space-m); }
.prose { max-inline-size: var(--measure); }

/* ---- HERO ---------------------------------------------------------------
   An empty wall with a name on it, weighted to the lower left, with the warm
   ochre wash behind the facts in the upper right. No rule, no horizon line,
   no scroll cue, no eyebrow, no portrait. */
.hero {
  display: grid;
  align-content: end;
  min-block-size: min(78svh, 46rem);
  padding-block: clamp(6rem, 4rem + 6vw, 9rem) var(--space-5xl);
}
.hero .wrap { align-items: end; }
.hero__lead {
  margin-block-start: var(--space-l);
  max-inline-size: 46ch;
  font-family: var(--font-display);
  font-size: var(--t-2);
  font-weight: var(--wt-regular);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-2);
  color: var(--ink);
}
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-l);
  margin-block-start: var(--space-2xl);
}
@media (min-width: 1024px) {
  }
@media (max-width: 1023px) {
  .hero { min-block-size: auto; padding-block: clamp(4rem, 2rem + 6vw, 7rem) var(--space-4xl); }
  .hero .wrap { align-items: start; row-gap: var(--space-2xl); }
}

/* ---- RIGHT NOW ---------------------------------------------------------
   Opens with the data, not with a paragraph. The margin holds a dateline, and
   the current focus reads across the page as four beats: one verb in the
   display face, the rest of the clause quiet beneath it. A ledger, not a
   third serif lead in a row. The small "Right now" sits on the baseline of
   the first verb; the date hangs under it. */
.section--now { padding-block: var(--space-3xl); }
.now__h { display: grid; row-gap: var(--space-2xs); }
.now__when {
  font-family: var(--font-display);
  font-size: var(--t-1);
  font-weight: var(--wt-regular);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-1);
  white-space: nowrap;
  color: var(--ink-strong);
}
.now__ledger {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-xl) var(--gutter);
}
.now__ledger li {
  display: grid;
  align-content: start;
  row-gap: var(--space-xs);
  max-inline-size: 30ch;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
  color: var(--ink-quiet);
}
.now__verb {
  font-family: var(--font-display);
  font-size: var(--t-3);
  font-weight: var(--wt-regular);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-3);
  color: var(--ink-strong);
}
@media (max-width: 1199px) { .now__ledger { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 1023px) {
  .now__h { margin-block-end: var(--space-xl); }
  .now__ledger { grid-column: 1 / -1; }
}
@media (max-width: 639px) {
  .now__ledger { grid-template-columns: minmax(0, 1fr); gap: var(--space-l); }
  .now__verb { font-size: var(--t-2); letter-spacing: var(--tr-2); }
}

/* ---- ABOUT -------------------------------------------------------------
   A painting hung in the right margin, beside a column of text, staying hung
   while you read past it. The section opens with one pulled line set from the
   margin edge, level with the top of the portrait, so the first thing you meet
   is a sentence and a face rather than a label and a paragraph. The prose
   under it is set in the text face. */
.about__pull {
  grid-column: 1 / 9;
  grid-row: 1;
  align-self: start;
  max-inline-size: 20ch;
  margin-block-end: var(--space-2xl);
  font-family: var(--font-display);
  font-size: var(--t-4);
  font-weight: var(--wt-regular);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-4);
  color: var(--ink-strong);
}
.section--about .marg,
.section--about .prose { grid-row: 2; }
@media (min-width: 1440px) {
  .about__pull { grid-column: 1 / 10; }
  .section--about .content { grid-column: 3 / 10; }
}
@media (min-width: 1024px) {
  .section--about .prose > p:first-child {
    font-size: var(--t-1);
    letter-spacing: var(--tr-1);
    color: var(--ink-strong);
  }
}
/* Narrow screens: the picture leads, under the heading and above the prose.
   order only moves the painted box; the document order, and therefore the
   reading and tab order, is unchanged, and the figure holds nothing focusable. */
@media (max-width: 1023px) {
  .about__pull {
    grid-column: 1 / -1;
    order: -3;
    margin-block-end: var(--space-xl);
    font-size: var(--t-3);
    letter-spacing: var(--tr-3);
  }
  .section--about .marg,
  .section--about .prose { grid-row: auto; }
  .section--about .marg { order: -2; }
}

/* ---- EXPERIENCE ---------------------------------------------------------
   A timeline that is one drawn line. Two or three current roles at full
   weight; the earlier ones compress into a quiet list. Round 1 gave seven
   roles identical weight across 2,642px, which is what made the page read
   as a student CV. */
.section--experience { padding-block: var(--section-y) var(--space-3xl); }

.rail { position: relative; padding-inline-start: var(--space-l); }
.rail::before {                        /* the line, one dragged vertical stroke. A border cannot scaleY. */
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -2px;
  inline-size: 5px;
  background: var(--stroke);
  mask: var(--brush-v) 0 0 / 100% 100% no-repeat;
  transform: scaleY(1);
  transform-origin: top;
}
.rail__item {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  column-gap: var(--space-l);
  padding-block-end: var(--space-3xl);
}
.rail__when {
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
  color: var(--ink-quiet);
}
.rail__role { margin-block-start: -0.14em; }
.rail__org {
  margin-block-start: var(--space-3xs);
  font-size: var(--t--1);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--1);
  color: var(--accent);
}
.rail__sum { margin-block-start: var(--space-s); max-inline-size: 60ch; }
.rail__points { margin-block-start: var(--space-m); display: grid; gap: var(--space-2xs); }
.rail__points li {
  position: relative;
  padding-inline-start: var(--space-m);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
  color: var(--ink-quiet);
}
.rail__points li::before {
  content: "";
  position: absolute;
  inset-block-start: 0.68em;
  inset-inline-start: 0;
  inline-size: 6px;
  block-size: 1px;
  background: var(--rule-strong);
}

/* The earlier roles: one row each, one hairline above the group, none
   between rows. Space separates them. */
.rail__past { position: relative; margin-block-start: var(--space-m); }
.rail__past li {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  column-gap: var(--space-l);
  padding-block: var(--space-s);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
}
.rail__past .row__when { color: var(--ink-quiet); }
.rail__past .row__what { color: var(--ink); }
.rail__past .row__org { color: var(--ink-quiet); }

@media (max-width: 639px) {
  .rail { padding-inline-start: var(--space-m); }
  .rail__item, .rail__past li { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); }
  .rail__when, .rail__past .row__when { font-size: var(--t--2); letter-spacing: var(--tr--2); }
  .rail__role { margin-block-start: 0; }
}

/* ---- PROJECTS -----------------------------------------------------------
   Three at three different spans, then a quiet index. No grid of equal
   cells anywhere, and no entrance animation: this section earns attention
   from composition, which is the harder and better way. */
.section--projects { padding-block: var(--space-3xl) var(--space-5xl); }

.feature { position: relative; }
.feature--1 { grid-column: 3 / -1; padding-block-end: var(--space-4xl); }
/* 2 and 3 share one row as a hung pair: four columns, one column of wall,
   five columns dropped a step lower. Neither has a picture, so neither is
   given a hole where one would go. */
.feature--2 { grid-column: 3 / 7;  align-self: start; padding-block-end: var(--space-3xl); }
.feature--3 { grid-column: 8 / 13; align-self: start; margin-block-start: var(--space-3xl); padding-block-end: var(--space-3xl); }

.feature__t {
  font-family: var(--font-display);
  font-weight: var(--wt-medium);
  line-height: var(--lh-tight);
  color: var(--ink-strong);
}
.feature--1 .feature__t { font-size: var(--t-4); letter-spacing: var(--tr-4); }
.feature--2 .feature__t,
.feature--3 .feature__t { font-size: var(--t-2); letter-spacing: var(--tr-2); }

.feature__blurb { margin-block-start: var(--space-s); max-inline-size: 48ch; }
.feature--1 .feature__blurb { font-size: var(--t-1); }
.feature--2 .feature__blurb,
.feature--3 .feature__blurb { font-size: var(--t-0); max-inline-size: 44ch; }

/* The stack is named in prose, not in chips. Chips are the card grid in
   miniature and a retrieval model cannot quote them. */
.feature__stack {
  margin-block-start: var(--space-s);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  color: var(--ink-quiet);
  max-inline-size: 44ch;
}

.feature__link {
  color: var(--ink-strong);
  transition: background-size var(--d-hover) var(--ease-out);
}
/* The whole block is the target; the ring stays on the title; the text
   stays selectable. */
.feature__link::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.feature__body { position: relative; z-index: 1; }

.index {
  position: relative;
  grid-column: 3 / 11;
}
.index li { position: relative; padding-block: var(--space-s); }
.index__t {
  font-size: var(--t-0);
  font-weight: var(--wt-medium);
  color: var(--ink-strong);
  transition: background-size var(--d-hover) var(--ease-out);
}
.index__b { color: var(--ink-quiet); font-size: var(--t--1); letter-spacing: var(--tr--1); }
.index__t::after { content: ""; position: absolute; inset: 0; }
/* The grid drops to six columns at 1023 and to one at 639, so every span above
   nine has to be released here or it addresses a column that no longer exists
   and forces an implicit one. The size hierarchy survives; only the spans go. */
@media (max-width: 1023px) {
  .feature--1, .feature--2, .feature--3, .index { grid-column: 1 / -1; }
  .feature--3 { margin-block-start: 0; }
}

/* ---- SKILLS -------------------------------------------------------------
   Plain English instead of a chip cloud. A model can quote a sentence
   verbatim; it cannot quote a <ul> of forty one-word <li>s, and neither can
   a human skimming in forty seconds. */
.section--skills { padding-block: var(--space-4xl); }
.clusters {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-xl) var(--gutter);
}
.cluster { font-size: var(--t-0); max-inline-size: 42ch; }
.cluster__n {
  font-family: var(--font-display);
  font-size: var(--t-1);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-1);
  color: var(--ink-strong);
}
/* The visible backing for every topic in Person.knowsAbout, set as the sentence
   it is, at body size. Fine print under a hairline is what filler looks like. */
.works-across {
  grid-column: 3 / -1;
  margin-block-start: var(--space-2xl);
  color: var(--ink-quiet);
  max-inline-size: var(--measure);
}
@media (max-width: 1023px) { .clusters, .works-across { grid-column: 1 / -1; } }
@media (max-width: 767px)  { .clusters { grid-template-columns: minmax(0, 1fr); } }

/* ---- EDUCATION AND AWARDS ----------------------------------------------
   The lull. Deliberately the quietest and shortest section on the page, set
   smaller than its neighbours, so that Contact lands harder. */
.section--education { padding-block: var(--space-3xl); }
.lull--a { grid-column: 3 / 8; }
.lull--b { grid-column: 8 / 13; }
.lull > h3 {
  font-family: var(--font-text);
  font-size: var(--t--2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
}
/* One hairline above each group, none between rows. */
.rows { position: relative; margin-block-start: var(--space-s); }
.rows li {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--space-m);
  padding-block: var(--space-s);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
}
.rows__when { color: var(--ink-quiet); }
.rows__what b { font-weight: var(--wt-medium); color: var(--ink-strong); }
.rows__sub { display: block; color: var(--ink-quiet); }
@media (max-width: 1023px) { .lull--a, .lull--b { grid-column: 1 / -1; } .lull--b { margin-block-start: var(--space-xl); } }
@media (max-width: 479px)  { .rows li { grid-template-columns: minmax(0, 1fr); gap: var(--space-3xs); } }

/* ---- FREQUENTLY ASKED ---------------------------------------------------
   The same faq[] array that emits the FAQPage JSON-LD. One source, two
   renderings, so structured data and visible copy cannot drift apart. */
.section--faq { padding-block: var(--space-4xl); }
.faq {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-xl) var(--gutter);
}
.qa { max-inline-size: 46ch; }
.qa h3 { font-size: var(--t-1); letter-spacing: var(--tr-1); }
.qa p {
  margin-block-start: var(--space-2xs);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-meta);
  color: var(--ink-quiet);
}
@media (max-width: 1023px) { .faq { grid-column: 1 / -1; } }
@media (max-width: 767px)  { .faq { grid-template-columns: minmax(0, 1fr); } }

/* ---- CONTACT ------------------------------------------------------------
   The deepest, warmest surface on the page, the largest type on the page,
   and the loneliest. The section does NOT invert the theme: round 1's
   near-white band at the bottom of a dark page read as a rendering bug. */
.section--contact { padding-block: var(--space-5xl); }
.contact__note {
  font-size: var(--t-1);
  color: var(--ink-quiet);
  max-inline-size: 46ch;
}
.contact__display {
  position: relative;
  inline-size: fit-content;
  max-inline-size: 100%;
  margin-block-start: var(--space-xl);
  font-family: var(--font-display);
  font-size: var(--t-5);
  font-weight: var(--wt-medium);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-5);
  overflow-wrap: anywhere;
}
.contact__display a {
  color: var(--ink-strong);
  text-decoration: none;
}
.contact__underline {
  display: block;
  inline-size: 100%;
  block-size: 0.7rem;
  margin-block-start: 0.08em;
  overflow: visible;
  color: var(--accent);
}
.contact__underline path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  vector-effect: non-scaling-stroke;
}
@media (hover: hover) and (pointer: fine) {
  .contact__display a:hover { color: var(--accent); }
}
.contact__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-l);
  margin-block-start: var(--space-2xl);
}
.contact__links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  color: var(--ink-quiet);
  transition: color var(--d-hover) ease, background-size var(--d-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .contact__links a:hover { color: var(--ink-strong); }
}
.contact__where {
  margin-block-start: var(--space-l);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  color: var(--ink-quiet);
  max-inline-size: 60ch;
}
@media (max-width: 639px) {
  .contact__display { font-size: var(--t-3); letter-spacing: var(--tr-3); }
}

/* ---- FOOTER -------------------------------------------------------------
   Three items. It is not interesting and it is not supposed to be. */
.site-footer { position: relative; }
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  padding-block: var(--space-l);
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  color: var(--ink-faint);
}
.site-footer ul { display: flex; flex-wrap: wrap; gap: var(--space-l); }
.site-footer a, .site-footer button {
  min-height: 32px;
  padding: 0;
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
  background: none;
  border: 0;
  cursor: pointer;
  transition: color var(--d-hover) ease, background-size var(--d-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .site-footer a:hover, .site-footer button:hover { color: var(--ink-strong); }
}
/* The theme toggle is the one control on the page that cannot work without
   JS, so it is not shown until scripts/mode.js has stamped data-js. */
:root:not([data-js]) .theme-toggle { display: none; }

/* Coarse pointers get 44px targets. This block has to come after every rule it
   overrides: the footer rule above sets 32px at equal specificity, and an
   earlier coarse block silently lost to it. Fine pointers keep the drawn sizes. */
@media (pointer: coarse) {
  .site-header__mark,
  .hero__actions .link-underline,
  .site-footer a,
  .site-footer button {
    display: inline-flex;
    align-items: center;
    min-block-size: 44px;
  }
  .site-header__mark { min-inline-size: 44px; }
  /* The address is inline, wraps at the @ and carries the drawn underline, so it
     grows its hit area with block padding, which does not move the line box. */
  .contact__display a { padding-block: max(0px, calc((48px - 1em) / 2)); }
}

/* =========================================================================
   THE MODE CHOOSER.  Closed and inert unless JS calls showModal().
   Two rows, not two equal columns: the asymmetry is the argument.
   ========================================================================= */
.chooser {
  /* The width is derived from the viewport HEIGHT as well as its width. The still
     is 16:10 and full-bleed inside the panel, so a narrower dialog is a shorter
     one: a 720px laptop gets a smaller copy of the same composition, with both
     choices on screen, instead of a picture that pushes the alternative below
     an unscrollable fold. --chrome is everything in the panel that is not the
     still or the padding, measured in the browser at each heading size. */
  --pad: clamp(1.5rem, 1rem + 2.4vw, 2.75rem);
  --chrome: 15.75rem;
  --edge: var(--space-m);
  border: 0;
  padding: 0;
  inline-size: min(
    52rem,
    calc(100vw - var(--gutter) * 2),
    max(20rem, calc((100dvh - var(--chrome) - var(--pad) * 2 - var(--edge) * 2) * 1.6 + var(--pad) * 2))
  );
  max-inline-size: none;
  max-block-size: none;
  background: transparent;
  color: var(--ink);
  overflow: visible;
}
.chooser::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
@media (prefers-reduced-transparency: reduce) {
  .chooser::backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; }
}

.chooser__panel {
  position: relative;
  isolation: isolate;
  /* Last resort only (landscape phones, 1280x600): the panel scrolls, not the page,
     which showModal() has made inert. The dialog itself stays overflow: visible so
     the panel's shadow is not clipped.
     ponytail: the grain ::after covers the first scrollport of a scrolled panel
     only; at 3.5% opacity that is invisible, a sticky grain layer if it ever shows. */
  max-block-size: calc(100dvh - var(--edge) * 2);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--pad);
  background-color: var(--raised);
  background-image:                    /* the panel is painted, like the page */
    radial-gradient(120% 90% at 8% -8%,
      color-mix(in oklab, var(--wash-a) 55%, transparent) 0, transparent 62%),
    radial-gradient(90% 80% at 96% 40%,
      color-mix(in oklab, var(--wash-b) 42%, transparent) 0, transparent 58%);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-s);      /* 4px. Smaller reads more expensive. */
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.32), 0 28px 64px -18px rgb(0 0 0 / 0.55);
}
.chooser__panel::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-size: 180px 180px;
}
.chooser__h {
  font-family: var(--font-display);
  font-size: var(--t-4);
  font-weight: var(--wt-medium);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-4);
  color: var(--ink-strong);
}

.choice { cursor: pointer; }

/* The headline path: a large image-led block. */
.choice--desktop {
  display: grid;
  inline-size: 100%;
  text-align: start;
  gap: var(--space-s);
  margin-block-start: var(--space-xl);
  padding: 0 0 var(--space-m);
  background: transparent;
  border: 0;
  border-block-end: 1px solid var(--rule-strong);
  color: var(--ink);
  transition: transform var(--d-press) var(--ease-out);
}
.choice--desktop:active { transform: scale(0.995); }
/* The still is painted underneath, so a missing file reads as a painted
   panel rather than as a broken image. The <img> carries alt="" for the
   same reason; the text below it names the destination. */
.choice__still {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background-color: var(--band-cv);
  background-image:
    radial-gradient(90% 110% at 20% 0%, color-mix(in oklab, var(--wash-a) 70%, transparent) 0, transparent 66%),
    radial-gradient(80% 90% at 88% 70%, color-mix(in oklab, var(--wash-b) 52%, transparent) 0, transparent 60%);
}
.choice__still img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.choice__t {
  font-family: var(--font-display);
  font-size: var(--t-2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-2);
  color: var(--ink-strong);
}
.choice__d { font-size: var(--t--1); letter-spacing: var(--tr--1); line-height: var(--lh-meta); color: var(--ink-quiet); }

/* The alternative: a real, focusable, 44px, underlined text control. Not a
   greyed-out second option. */
.choice--plain {
  display: inline-flex;
  align-items: center;
  padding: 0;
  min-block-size: 44px;
  background: none;
  border: 0;
  color: var(--ink);
  font-size: var(--t-0);
  text-align: start;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: text-decoration-thickness var(--d-hover) ease;
}
@media (hover: hover) and (pointer: fine) {
  .choice--plain:hover { text-decoration-thickness: 2px; }
}

.chooser__foot {                        /* the alternative and the checkbox share a row */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  column-gap: var(--space-l);
  margin-block-start: var(--space-s);
}
@media (max-height: 760px) {
  .chooser { --pad: clamp(1.25rem, 0.75rem + 1.6vw, 2rem); --chrome: 14rem; }
  .chooser__h { font-size: var(--t-3); letter-spacing: var(--tr-3); }
  .choice--desktop { margin-block-start: var(--space-l); }
}
/* Short AND wide (1280x600, 900x560, a laptop with the bookmarks bar open): rows
   cannot fit, so the still and its caption sit side by side. Still the headline
   path, still image-led, and both choices stay on screen. */
@media (max-height: 640px) and (min-width: 600px) {
  .chooser { inline-size: min(52rem, calc(100vw - var(--gutter) * 2)); }
  .choice--desktop {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    grid-template-rows: 1fr auto;
    column-gap: var(--space-l);
  }
  .choice__still { grid-row: 1 / span 2; }
  .choice__t { align-self: end; }
}
.chooser__remember {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  color: var(--ink-quiet);
  cursor: pointer;
}
.chooser__remember input { accent-color: var(--accent); width: 1rem; height: 1rem; }

/* Materialize: a surface arrives as a material, not as a plain opacity fade.
   Enter 260ms, exit 180ms. Exit is always faster than enter. Never scale(0). */
.chooser[open]           { animation: chooser-in var(--d-enter) var(--ease-drawer) both; }
.chooser[open]::backdrop { animation: scrim-in 180ms var(--ease-out) both; }
@keyframes chooser-in { from { opacity: 0; transform: translateY(8px) scale(0.985); filter: blur(6px); } }
@keyframes scrim-in   { from { opacity: 0; } }

.chooser.is-closing           { animation: chooser-out var(--d-exit) var(--ease-out) both; }
.chooser.is-closing::backdrop { animation: scrim-in 180ms var(--ease-out) reverse both; }
@keyframes chooser-out { to { opacity: 0; transform: translateY(4px) scale(0.99); filter: blur(4px); } }
@keyframes chooser-fade { from { opacity: 0; } }

/* =========================================================================
   MODE GATING.  This is what prevents FOUC, with zero inline CSS.
   ========================================================================= */
:root[data-mode="desktop"] #main,
:root[data-mode="desktop"] .site-header,
:root[data-mode="desktop"] .site-footer { display: none; }
:root:not([data-mode="desktop"]) .xp-root { display: none; }   /* no-JS: never show XP */

/* XP-ROOT SIZING - DO NOT DELETE, DO NOT MERGE AWAY.
   styles/main.css sets `.desktop { height: 100%; overflow: hidden }`. Round 1
   wrapped the XP markup in this .xp-root div, which has no height of its own,
   so that 100% resolved to 0px and every icon and the wallpaper were clipped;
   only the position:fixed taskbar and Clippy still showed, which is why it
   looked half-loaded rather than broken. This one rule is the percent-height
   chain. Keep it next to the display:none rule above. */
:root[data-mode="desktop"] .xp-root {
  block-size: 100vh;
  block-size: 100dvh;
}
/* The painted ground and the grain are for the one-pager. In desktop mode the
   XP wallpaper owns the surface, and two fixed layers under it are wasted paint. */
:root[data-mode="desktop"] body::before,
:root[data-mode="desktop"] body::after { display: none; }

.desktop-loading {
  position: fixed;
  inset: 0;
  z-index: 99998;
  display: grid;
  place-content: center;
  gap: var(--space-m);
  justify-items: center;
  background: var(--ground);
  color: var(--ink-quiet);
  font-size: var(--t--1);
  text-align: center;
  padding: var(--gutter);
}
.desktop-loading p { max-inline-size: 28ch; }
.desktop-loading[hidden] { display: none; }

.mode-error {
  max-inline-size: var(--wrap-max);
  margin: var(--space-m) auto 0;
  padding: var(--space-s) var(--gutter);
  border-inline-start: 1px solid var(--accent);
  font-size: var(--t--1);
  color: var(--ink-quiet);
}

/* =========================================================================
   THE HAND.  The CSP has no 'unsafe-inline' and no 'unsafe-hashes', so the
   generator cannot emit style="--i:3". The stagger index, the jitter and the
   tilt are therefore authored here, by hand, once, in the place where taste
   belongs, and they are deterministic across loads.

   No two gaps are equal and the tilts do not alternate in a pattern the eye
   can lock onto. Tilt is capped at 0.31deg (sub-half-degree, so it never
   reads as "rotated", only as "hand-hung") and is applied to the CV sheets
   and the portrait only, never to a row of body text.

   The jitter is expressed as a scroll-range offset (--jit-r) rather than as a
   time delay, because animation-delay is ignored on a view() timeline: on a
   scroll timeline the animation's position IS the scroll position, so the only
   way to make one item arrive a beat after its neighbour is to move its range.
   Base step is 3 percentage points of the entry range, jittered by -1 to +2,
   which is the scroll-domain equivalent of a 52ms step with -9 to +21ms of
   hand. The table wraps with 9n+k so a tenth item never loses its offset.
   ========================================================================= */
.stagger > *:nth-child(9n+1) { --jit-r:  0%; --tilt: -0.24deg; }
.stagger > *:nth-child(9n+2) { --jit-r:  3%; --tilt:  0.15deg; }
.stagger > *:nth-child(9n+3) { --jit-r:  5%; --tilt: -0.08deg; }
.stagger > *:nth-child(9n+4) { --jit-r:  9%; --tilt:  0.29deg; }
.stagger > *:nth-child(9n+5) { --jit-r: 11%; --tilt: -0.17deg; }
.stagger > *:nth-child(9n+6) { --jit-r: 14%; --tilt:  0.22deg; }
.stagger > *:nth-child(9n+7) { --jit-r: 18%; --tilt: -0.31deg; }
.stagger > *:nth-child(9n+8) { --jit-r: 20%; --tilt:  0.11deg; }
.stagger > *:nth-child(9n+9) { --jit-r: 23%; --tilt: -0.19deg; }

/* =========================================================================
   MOTION.  Seven authored moments on the whole page, each with a named
   purpose. Everything else is stillness on purpose. Every animation is
   written `from { ... }` with the declared value as the finished state, so
   with no JS, in Firefox, and for a crawler the page is simply finished.
   There is no IntersectionObserver fallback and that is deliberate.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {

  /* 1. The name paints on. Purpose: explanation. Rare / first load only.
        A clean gradient edge reads as a wipe; a brush needs hairs.

        ONE mask layer, not two. The bristles are baked INTO the sweep tile
        instead of being a second layer intersected with it, because a second
        uniform texture layer at mask-size 100% 100% is composited for the
        whole life of the element: it roughens the finished glyphs, not the
        travelling edge. (That shipped, and "Maj Korent" rendered eroded in
        its finished state.) Here the tile is, left to right: solid, a
        bristly falloff, transparent. The falloff is the only rough part and
        it travels off the right-hand side, so the end state is unmasked
        type.

        The tile's alpha is  clamp(2.4 * gradient - 1.4 * bristle).  Where the
        gradient is 1 (tile fraction <= 0.54) the result is >= 1.0 for every
        possible bristle value, so that region is provably solid. At the
        finished position the element spans tile fraction 0 .. 1/2.2 = 0.455,
        which is inside that region with room to spare. Everything between
        0.54 and 0.78 is the brush edge.

        mask-position percentages resolve against (element - mask), which is
        negative here (the mask is 220% wide), so 150% puts the tile far to
        the LEFT of the element and 0 puts its solid end over all of it. The
        band therefore crosses left to right and exits.

        Vertical baseFrequency is 0.11 (not 0.38): the tile height always maps
        to the element height, so at the 44px hero of a 360px viewport 0.38
        resolves to sub-pixel streaks that read as video static rather than as
        hairs. 0.11 gives roughly 4px hairs at 360 and 8px at 1440. */
  .hero__name {
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='120'%3E%3Cfilter id='b' filterUnits='userSpaceOnUse' x='0' y='0' width='600' height='120' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.004 0.11' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.9 -0.62' result='h'/%3E%3CfeComposite in='SourceGraphic' in2='h' operator='arithmetic' k1='0' k2='2.4' k3='-1.4' k4='0'/%3E%3C/filter%3E%3ClinearGradient id='g'%3E%3Cstop offset='0.54' stop-color='%23000'/%3E%3Cstop offset='0.78' stop-color='%23000' stop-opacity='0'/%3E%3C/linearGradient%3E%3Crect width='600' height='120' fill='url(%23g)' filter='url(%23b)'/%3E%3C/svg%3E");
    mask-size: 220% 100%;
    mask-position: 0 0;
    mask-repeat: no-repeat;
    animation: name-paint var(--d-paint) var(--ease-wash) both;
  }
  @keyframes name-paint { from { mask-position: 150% 0; } }

  @supports (animation-timeline: view()) {

    /* 3. The rail draws itself. Purpose: spatial consistency. It is a
          timeline, so the rule traces time. */
    .rail::before {
      animation: rail-draw 1ms linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 45%;
    }
    @keyframes rail-draw { from { transform: scaleY(0); } }

    /* 4. Current roles arrive. Purpose: preventing a jarring change.
          Staggered by the hand table above. */
    .rail__item {
      animation: arrive 320ms var(--ease-out) both;
      animation-timeline: view();
      animation-range: entry calc(6% + var(--jit-r, 0%)) entry calc(48% + var(--jit-r, 0%));
    }
    @keyframes arrive { from { opacity: 0; transform: translateY(10px); } }

    @keyframes portrait-wash { from { mask-size: 46% 46%, 100% 100%; } }

    /* 5. The contact underline draws itself. Purpose: emphasis. */
    .contact__underline path {
      animation: underline-draw 1ms linear both;
      animation-timeline: view();
      animation-range: entry 24% entry 74%;
    }
    @keyframes underline-draw { from { stroke-dashoffset: 1; } }
  }
}

/* Reduced motion means fewer and gentler, never zero. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Re-grant the three that aid comprehension rather than decorate. */
  .chooser[open], .chooser.is-closing,
  .chooser[open]::backdrop, .chooser.is-closing::backdrop { animation-duration: 120ms !important; }
  .chooser[open]      { animation-name: chooser-fade; }
  .chooser.is-closing { animation-name: chooser-fade; animation-direction: reverse; }
  .btn, .choice, .mode-switch { transition-duration: 100ms !important; }
}

/* =========================================================================
   XP DESKTOP MARKUP SHIMS.
   CSP forbids style="" attributes, so the inline styles that used to live in
   index.html's XP templates became these classes. They belong in
   styles/windows.css; they sit here only because index.html and that
   stylesheet have different owners. Move them on the next desktop pass.
   Every selector is desktop-only.
   ========================================================================= */
.xp-row { display: flex; align-items: center; gap: 8px; }
.xp-row input[type="range"] { flex: 1; cursor: pointer; }
.xp-value { min-width: 50px; text-align: right; font-weight: bold; font-family: ui-monospace, Consolas, monospace; }
.xp-center { text-align: center; padding: 10px; }
.xp-scrollbox { overflow: auto; max-height: 350px; }
.xp-statusbar { padding: 10px; text-align: center; border-top: 1px solid #333; background: #1a1a1a; color: #999; }
.window-body.window-body--flush { padding: 0; overflow-y: auto; max-height: 500px; }
.window-title.window-title--alert { background: #ff0000; }

/* =========================================================================
   ROUND 3.  The wallpaper, the drawings, the free-stuff shelf and the
   contribution grid.
   ========================================================================= */

/* ---- THE INTERACTIVE WALLPAPER -----------------------------------------
   One canvas, painted by scripts/wallpaper.js. It sits above the static
   gradient layer (body::before, z -2) and below the grain (body::after,
   z -1, painted after element children at the same index), so the three
   surfaces stack in one order: gradient, moving pigment, grain, page.
   Without JavaScript or WebGL it stays transparent and the gradient is the
   whole design, which is why nothing on the page reads from it. */
.wallpaper {
  position: fixed;
  inset: 0;
  z-index: -1;
  inline-size: 100%;
  block-size: 100%;
  display: block;
  pointer-events: none;
  /* The paint is a wash, not a picture: never let it sharpen into bands. */
  filter: blur(11px);
  opacity: 0.95;
}
/* Desktop mode has its own wallpaper, and two is one too many. */
:root[data-mode="desktop"] .wallpaper { display: none; }
@media (prefers-reduced-transparency: reduce) { .wallpaper { display: none; } }

/* ---- DRAWINGS -----------------------------------------------------------
   Ink line art, roughened by one shared SVG filter. currentColor, so each
   drawing takes the colour of whatever it is sitting next to, and both
   themes are free. */
.u-defs { position: absolute; inline-size: 0; block-size: 0; overflow: hidden; }
.drawing {
  display: block;
  inline-size: 100%;
  block-size: auto;
  color: var(--ink-quiet);
  opacity: 0.85;
}
@media (forced-colors: active) { .drawing { filter: none; } }

@media (max-width: 1023px) {
  }

/* ---- FREE STUFF ---------------------------------------------------------
   The point of the page. Four shelves, each introduced by a drawing and a
   line saying who it is for, then the things themselves as a plain list of
   links. No cards: a card grid of identical rounded rectangles is the exact
   look this page is not. */
.section--resources { padding-block: var(--space-4xl); }
.resources__intro {
  max-inline-size: 54ch;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  color: var(--ink-quiet);
}
.shelves {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3xl) var(--gutter);
  margin-block-start: var(--space-3xl);
}
.shelf__head {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  margin-block-end: var(--space-xs);
}
.shelf__mark { inline-size: 2.75rem; flex: none; color: var(--accent); opacity: 0.9; }
.shelf__t {
  font-family: var(--font-display);
  font-size: var(--t-2);
  font-weight: var(--wt-regular);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-2);
  color: var(--ink-strong);
}
.shelf__note {
  max-inline-size: 42ch;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}
.shelf__list { display: grid; gap: var(--space-l); margin-block-start: var(--space-l); }
.give { display: grid; gap: var(--space-3xs); }
.give__t {
  font-size: var(--t-0);
  font-weight: var(--wt-medium);
  color: var(--ink-strong);
}
.give__b {
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}
@media (max-width: 1023px) { .shelves { grid-column: 1 / -1; } }
@media (max-width: 767px)  { .shelves { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xl); } }

/* ---- CONTRIBUTIONS ------------------------------------------------------
   A year of days as 371 squares. The number is the point, so it is set in
   the display face at reading size and the grid is the evidence under it. */
.section--contributions { padding-block: var(--space-4xl); }
.contrib__lead {
  max-inline-size: 60ch;
  font-size: var(--t-0);
  line-height: var(--lh-body);
  color: var(--ink);
}
.contrib__lead b {
  font-family: var(--font-display);
  font-size: var(--t-3);
  font-weight: var(--wt-regular);
  letter-spacing: var(--tr-3);
  color: var(--ink-strong);
}
.contrib { margin: var(--space-xl) 0 0; }
/* The grid keeps its aspect ratio and scrolls rather than squashing: a
   contribution square that is 3px wide says nothing. */
.contrib__grid {
  display: block;
  inline-size: 100%;
  min-inline-size: 34rem;
  block-size: auto;
}
.contrib > .contrib__grid { min-inline-size: 0; }
@media (max-width: 767px) {
  .contrib { overflow-x: auto; overscroll-behavior-inline: contain; }
  .contrib__grid { min-inline-size: 34rem; }
}
.heat { fill: var(--heat-0); }
.heat--1 { fill: var(--heat-1); }
.heat--2 { fill: var(--heat-2); }
.heat--3 { fill: var(--heat-3); }
.heat--4 { fill: var(--heat-4); }
.contrib__key {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-s);
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
}
.heat-key { display: inline-flex; gap: 3px; }
.heat-key .heat {
  inline-size: 8px;
  block-size: 8px;
  border-radius: 1.6px;
  background: var(--heat-0);
}
.heat-key .heat--1 { background: var(--heat-1); }
.heat-key .heat--2 { background: var(--heat-2); }
.heat-key .heat--3 { background: var(--heat-3); }
.heat-key .heat--4 { background: var(--heat-4); }
.contrib__h3 {
  margin-block-start: var(--space-2xl);
  font-size: var(--t--2);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr--2);
  color: var(--ink-quiet);
}
.repos { display: grid; gap: var(--space-s); margin-block-start: var(--space-m); }
.repos li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-s);
}
.repos__t { font-size: var(--t-0); color: var(--ink-strong); }
.repos__n { font-size: var(--t--2); letter-spacing: var(--tr--2); color: var(--ink-quiet); }
.contrib__src {
  margin-block-start: var(--space-xl);
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  color: var(--ink-faint);
}

/* ---- LETTERS THAT BOUNCE ------------------------------------------------
   scripts/bounce.js splits every display heading into letters and springs
   them away from the pointer. Only transform is animated, and the spans
   exist only while that script runs: without it every heading is one text
   node, exactly as the HTML shipped. */
.bnc-w { display: inline-block; white-space: nowrap; }
.bnc {
  display: inline-block;
  will-change: transform;
}

/* ---- AWAY FROM THE KEYBOARD --------------------------------------------
   Five drawn vignettes. They are the one place on the page where the ink is
   the content rather than a mark beside it, so they get the accent and more
   room than the shelf marks. */
.section--elsewhere { padding-block: var(--space-4xl); }
.likes {
  grid-column: 3 / -1;
  display: grid;
  /* Six of them: three across reads as a set, five across leaves an orphan. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3xl) var(--gutter);
}
.like { display: grid; gap: var(--space-2xs); align-content: start; }
.like__art {
  inline-size: 100%;
  max-inline-size: 8.5rem;
  margin-block-end: var(--space-xs);
  color: var(--accent);
  opacity: 0.92;
}
.like__t {
  font-family: var(--font-display);
  font-size: var(--t-1);
  font-weight: var(--wt-regular);
  letter-spacing: var(--tr-1);
  color: var(--ink-strong);
}
.like__l {
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
  max-inline-size: 24ch;
}
@media (max-width: 1023px) { .likes { grid-column: 1 / -1; row-gap: var(--space-2xl); } }
@media (max-width: 639px)  { .likes { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---- FREQUENTLY ASKED, FOLDED ------------------------------------------
   Every answer stays in the DOM for crawlers and for the FAQPage JSON-LD;
   a reader sees seven questions instead of seven paragraphs. */
details.qa { border-block-start: 1px solid var(--rule); padding-block: var(--space-s); }
details.qa:last-child { border-block-end: 1px solid var(--rule); }
details.qa summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: var(--space-s);
  font-size: var(--t-0);
  color: var(--ink-strong);
}
details.qa summary::-webkit-details-marker { display: none; }
details.qa summary::after {
  content: "+";
  margin-inline-start: auto;
  font-size: var(--t-1);
  line-height: 1;
  color: var(--ink-quiet);
  transition: transform var(--d-ui) var(--ease-out);
}
details.qa[open] summary::after { content: "\2013"; }
@media (hover: hover) and (pointer: fine) {
  details.qa summary:hover { color: var(--accent); }
}
details.qa p {
  max-inline-size: 60ch;
  margin-block-start: var(--space-s);
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}

/* ---- THE CHOOSER, FULL SCREEN, SPLIT IN TWO -----------------------------
   Two halves, equal weight, each the whole height of the screen: the choice
   is the first thing this site does, so it gets the whole first screen
   rather than a card in the middle of one. */
.chooser {
  inline-size: 100vw;
  max-inline-size: none;
  block-size: 100dvh;
  max-block-size: none;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  color: var(--ink);
  background: var(--ground);
}
.chooser__h {
  position: absolute;
  inset-block-start: clamp(1.25rem, 3vh, 2.5rem);
  inset-inline: 0;
  z-index: 2;
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--t-1);
  font-weight: var(--wt-regular);
  color: var(--ink-quiet);
}
.chooser__split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  block-size: 100%;
}
.half {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-l);
  padding: clamp(3rem, 8vh, 6rem) clamp(1.5rem, 4vw, 4rem);
  font: inherit;
  text-align: center;
  color: var(--ink);
  background: none;
  border: 0;
  cursor: pointer;
  overflow: hidden;
  transition: background-color var(--d-ui) var(--ease-out);
}
.half + .half { box-shadow: inset 1px 0 0 var(--rule); }
.half__still {
  display: block;
  inline-size: min(100%, 30rem);
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border: 1px solid var(--rule-strong);
  box-shadow: 0 18px 50px -30px rgb(0 0 0 / 0.5);
  transition: transform var(--d-ui) var(--ease-out);
}
.half__still img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
/* The two halves have to carry the same weight, or the choice is not a choice:
   the page side gets a frame of its own, the same size as the XP still, with
   the drawing sitting inside it. */
.half__art {
  display: grid;
  place-items: center;
  inline-size: min(100%, 30rem);
  aspect-ratio: 16 / 10;
  color: var(--accent);
  border: 1px solid var(--rule-strong);
  background: color-mix(in oklab, var(--accent) 5%, transparent);
  transition: transform var(--d-ui) var(--ease-out);
}
.half__art svg { inline-size: clamp(5rem, 11vh, 8.5rem); block-size: auto; display: block; }
.half__body { display: grid; gap: var(--space-xs); max-inline-size: 34ch; }
.half__t {
  font-family: var(--font-display);
  font-size: var(--t-3);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-3);
  color: var(--ink-strong);
}
.half__d {
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}
.half__go {
  justify-self: center;
  margin-block-start: var(--space-s);
  padding: var(--space-xs) var(--space-l);
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  color: var(--ink-strong);
  border: 1px solid var(--border-ctrl);
  border-radius: var(--radius-pill);
  transition: background-color var(--d-hover) ease, color var(--d-hover) ease;
}
@media (hover: hover) and (pointer: fine) {
  .half:hover { background: color-mix(in oklab, var(--accent) 7%, transparent); }
  .half:hover .half__still { transform: scale(1.03) rotate(-0.4deg); }
  .half:hover .half__art { transform: translateY(-6px) rotate(2deg); }
  .half:hover .half__go { background: var(--ink-strong); color: var(--ground); }
}
.half:focus-visible { outline: 2px solid var(--focus); outline-offset: -6px; }
.chooser__remember {
  position: absolute;
  inset-block-end: clamp(1rem, 3vh, 2rem);
  inset-inline: 0;
  z-index: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--t--2);
  color: var(--ink-quiet);
}
@media (max-width: 767px) {
  .chooser__split { grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr 1fr; }
  .half + .half { box-shadow: inset 0 1px 0 var(--rule); }
  .half { gap: var(--space-m); padding-block: var(--space-xl); }
  .half__t { font-size: var(--t-2); }
  .half__still { inline-size: min(100%, 18rem); }
  .half__art { inline-size: min(100%, 18rem); }
}
@media (prefers-reduced-motion: reduce) {
  .half__still, .half__art { transition: none; }
}

/* =========================================================================
   MAJ'S MOTION.  The numbers in this block are lifted from the way he already
   animates wminus.com (src/components/Motion.astro, Nav.astro, the glass-card
   rule that repeats on every page there): reveal y 24 -> 0 over .7s on a
   power2.out curve at 88% of the viewport, .08s stagger, tilt +-7deg through
   a 900px perspective, a 220px cursor glow, and the spring underline. The
   engine is scripts/motion.js; these are the surfaces it drives.
   ========================================================================= */

/* power2.out and his overshoot curve, named once so the values stay honest. */
/* His two named curves, spelled the way he spells them (Golobi custom-css line 14-15):
   --ease settles shadows and opacity, --spring carries anything that moves. He assigns them
   per property on the same rule rather than one blanket transition, so a card's shadow lands
   while its transform is still overshooting. */
:root {
  --ease:   cubic-bezier(0.32, 0.72, 0, 1);
  --spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-power2: cubic-bezier(0.215, 0.61, 0.355, 1);
  --ease-glass:  cubic-bezier(0.165, 0.84, 0.44, 1);
}

/* ---- reveal on scroll --------------------------------------------------- */
.rv {
  opacity: 0;
  transform: translateY(26px);
  filter: blur(5px);
  transition: opacity 600ms ease-out, transform 600ms var(--spring), filter 600ms ease-out;
  transition-delay: calc(var(--rv-i, 0) * 80ms);
}
.rv.is-in { opacity: 1; transform: none; filter: none; }
@media (prefers-reduced-motion: reduce) {
  .rv { opacity: 1; transform: none; transition: none; }
}

/* ---- parallax layers ---------------------------------------------------- */
[data-parallax] { display: block; will-change: transform; }

/* ---- 3D tilt ------------------------------------------------------------ */
[data-tilt] {
  transform-style: preserve-3d;
  will-change: transform;
  transition: transform 600ms var(--ease-glass),
              border-color 500ms var(--ease-glass),
              box-shadow 500ms var(--ease-glass);
}

/* ---- the cursor glow ---------------------------------------------------- */
[data-glow] { position: relative; }
[data-glow]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  background: radial-gradient(220px circle at var(--mx, 50%) var(--my, 50%),
              color-mix(in oklab, var(--accent) 12%, transparent), transparent 65%);
  transition: opacity 400ms ease;
}
@media (hover: hover) and (pointer: fine) {
  [data-glow]:hover::after { opacity: 1; }
}
[data-glow] > * { position: relative; z-index: 1; }

/* ---- the interest cards get a surface, so the tilt has something to tilt -- */
.like {
  padding: var(--space-l);
  border: 1px solid var(--rule);
  border-radius: 1rem;                    /* his one radius */
  background: color-mix(in oklab, var(--raised) 55%, transparent);
}
@media (hover: hover) and (pointer: fine) {
  .like:hover { border-color: color-mix(in oklab, var(--accent) 38%, var(--rule)); }
}
.like__artwrap, .shelf__markwrap { display: block; }
.shelf__markwrap { inline-size: 2.75rem; flex: none; }

/* ---- the header steps out of the way ------------------------------------
   Nav.astro on wminus.com: visible at the top and while scrolling, gone half a
   second after the scroll stops, back the moment you move or hover it. */
.site-header {
  transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1),
              background-color var(--d-ui) ease;
}
.site-header.is-away { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) { .site-header.is-away { transform: none; } }

/* ---- the spring underline ------------------------------------------------ */
.site-nav a { position: relative; }
.site-nav a::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 2px;
  block-size: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 280ms var(--spring);
}
@media (hover: hover) and (pointer: fine) {
  .site-nav a:hover::after { transform: scaleX(1); }
}
.site-nav a:focus-visible::after { transform: scaleX(1); }

/* ---- Konami ------------------------------------------------------------- */
.is-storm .wallpaper { opacity: 1; filter: blur(7px) saturate(1.25); }
.is-storm .drawing { animation: storm-wobble 900ms var(--spring) infinite alternate; }
@keyframes storm-wobble {
  from { transform: rotate(-2.5deg) translateY(-2px); }
  to   { transform: rotate(2.5deg) translateY(2px); }
}
@media (prefers-reduced-motion: reduce) {
  .is-storm .drawing { animation: none; }
}

/* ---- two more of his habits ---------------------------------------------- */
h1, h2, h3, .hero__lead, .about__pull { text-wrap: balance; }
.num, .contrib__lead b, .repos__n { font-variant-numeric: tabular-nums; }

/* ---- The marquee -------------------------------------------------------
   Lifted from his Golobi stylesheet: duplicated track, translate3d(0) to
   translate3d(-50%), 32s linear infinite, paused on hover, both edges eaten
   by a mask. The only change is the direction of the fade, because this
   ground is paper rather than white. */
.mq {
  grid-column: 1 / -1;
  overflow: hidden;
  margin-block-end: var(--space-2xl);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.mq__t {
  display: flex;
  align-items: center;
  gap: 3.5rem;
  padding-inline-end: 3.5rem;
  inline-size: max-content;
  animation: mq-scroll 32s linear infinite;
}
.mq:hover .mq__t { animation-play-state: paused; }
.mq__i {
  font-family: var(--font-display);
  font-size: var(--t-2);
  color: var(--ink-faint);
  white-space: nowrap;
}
@keyframes mq-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .mq__t { animation: none; }
  .mq { mask-image: none; -webkit-mask-image: none; }
}

/* ---- Hover lift ---------------------------------------------------------
   His card hover everywhere: a few pixels up, an inset highlight along the
   top edge and one soft coloured glow underneath. Transform on --spring,
   shadow on --ease, which is the split he always makes. */
.like, .shelf {
  transition: transform 500ms var(--spring), box-shadow 500ms var(--ease),
              border-color 500ms var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .like:hover {
    transform: translateY(-4px);
    box-shadow: inset 0 1px 0 color-mix(in oklab, var(--raised) 90%, white),
                0 18px 40px -26px color-mix(in oklab, var(--accent) 55%, transparent);
  }
}

/* ---- Breathing ----------------------------------------------------------
   The ambient loop he puts in every idle screen: a slow opacity pulse,
   ease-in-out, infinite, a couple of seconds. Here it is the drawings, each
   one offset so they never breathe in unison. */
@media (prefers-reduced-motion: no-preference) {
  .like__art, .shelf__mark { animation: breathe 3600ms ease-in-out infinite; }
  .like:nth-child(2n) .like__art { animation-delay: -900ms; }
  .like:nth-child(3n) .like__art { animation-delay: -1800ms; }
  .shelf:nth-child(2n) .shelf__mark { animation-delay: -1200ms; }
}
@keyframes breathe {
  0%, 100% { opacity: 0.92; }
  50%      { opacity: 1; }
}

/* ---- A PAGE OF ITS OWN --------------------------------------------------
   Every section that used to be a stop on one long scroll is now a page, so
   each one opens the way the home page does: a name, a line, then the thing
   itself. */
.section--pagehead { padding-block: clamp(5rem, 4rem + 6vw, 9rem) var(--space-2xl); }
.page__h {
  grid-column: 1 / -1;
  font-family: var(--font-display);
  font-size: var(--t-4);
  font-weight: var(--wt-regular);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-4);
  color: var(--ink-strong);
}
.page__lead {
  grid-column: 3 / -1;
  max-inline-size: 54ch;
  margin-block-start: var(--space-m);
  font-family: var(--font-display);
  font-size: var(--t-1);
  line-height: var(--lh-snug);
  color: var(--ink);
}
@media (max-width: 1023px) { .page__lead { grid-column: 1 / -1; } }

/* ---- Room for the pen ---------------------------------------------------
   A note in the margin needs a margin. The hero and the bands already lay
   their content out on a grid that leaves one, so all these need is a
   positioning context to hang off. */
.hero .wrap, .section .wrap { position: relative; }

/* ---- THE RECORD -----------------------------------------------------------
   Six facts, set like a ledger: the number large in the display face, the
   label small in the text face, a hairline between rows. No cards, no
   backgrounds. A brag typeset quietly is the only kind that reads as true. */
.section--record { padding-block: var(--space-3xl) var(--space-4xl); }
.record__sum {
  max-inline-size: 62ch;
  margin-block-end: var(--space-2xl);
  font-family: var(--font-display);
  font-size: var(--t-1);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-1);
  color: var(--ink);
}
.record {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: 0;
  padding: 0;
  list-style: none;
  border-block-start: 1px solid var(--rule-strong);
}
.rec {
  padding-block: var(--space-l) var(--space-xl);
  border-block-end: 1px solid var(--rule);
}
.rec__big {
  font-family: var(--font-display);
  font-size: var(--t-4);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-4);
  color: var(--ink-strong);
}
.rec__label {
  margin-block-start: var(--space-2xs);
  font-size: var(--t--2);
  letter-spacing: var(--tr--2);
  text-transform: uppercase;
  color: var(--accent);
}
.rec__detail {
  margin-block-start: var(--space-s);
  max-inline-size: 30ch;
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}
@media (max-width: 1023px) { .record { grid-column: 1 / -1; } }
@media (max-width: 767px)  { .record { grid-template-columns: minmax(0, 1fr); } }

/* ---- ELSEWHERE ------------------------------------------------------------
   Six lines. A page title in the display face, one sentence after it. */
.section--else { padding-block: var(--space-3xl) var(--space-4xl); }
.else { padding: 0; list-style: none; border-block-start: 1px solid var(--rule-strong); }
.else li {
  display: grid;
  grid-template-columns: minmax(12ch, 18ch) minmax(0, 1fr);
  column-gap: var(--gutter);
  align-items: baseline;
  padding-block: var(--space-m);
  border-block-end: 1px solid var(--rule);
}
.else__t {
  font-family: var(--font-display);
  font-size: var(--t-2);
  letter-spacing: var(--tr-2);
  color: var(--ink-strong);
  text-decoration: none;
  transition: color 280ms var(--ease);
}
.else__t:hover { color: var(--accent); }
.else__l {
  font-size: var(--t--1);
  letter-spacing: var(--tr--1);
  line-height: var(--lh-body);
  color: var(--ink-quiet);
}
@media (max-width: 767px) {
  .else li { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-2xs); }
}

/* ---- The line under the name ------------------------------------------- */
.hero__sub {
  margin-block-start: var(--space-xs);
  font-size: var(--t-0);
  letter-spacing: var(--tr--1);
  color: var(--ink);
}
.hero__where { color: var(--ink-quiet); }
.hero__where::before { content: " \00b7 "; }
