/*
 * style.css — everything visual that isn't the flock.
 *
 * Two themes, one hue. The accent is a single OKLCH hue (--hue) that drifts
 * across the day (see main.js: hueAt). Because lightness and chroma are fixed
 * per theme, contrast stays constant at every hour — AA is a property of the
 * design, not of the time you happened to visit.
 */

@font-face {
  font-family: 'Geist';
  src: url('../fonts/geist.woff2') format('woff2');
  font-weight: 400 500;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('../fonts/geist-mono.woff2') format('woff2');
  font-weight: 400 500;
  font-display: swap;
}
/* Metric-compatible fallback so text doesn't jump when Geist arrives. */
@font-face {
  font-family: 'Geist Fallback';
  src: local('Helvetica Neue'), local('Arial');
  size-adjust: 97.5%;
  ascent-override: 95%;
  descent-override: 24%;
  line-gap-override: 0%;
}

@property --hue { syntax: '<number>'; inherits: true; initial-value: 88; }

:root {
  /* One palette, written once. Every token names its light value and its dark
   * value in the same place, and `light-dark()` picks between them from the
   * element's used `color-scheme` — so the two themes can't drift apart in a
   * copy-paste, which is exactly what a duplicated dark block invites. The
   * manual override below therefore sets only `color-scheme`: switching the
   * scheme switches every token AND the UA's own surfaces (scrollbars, form
   * controls, the caret) in one move, which the old token-only override
   * silently left behind on the wrong theme. */
  color-scheme: light dark;
  --hue: 88;                         /* the 2013 yellow, until JS knows the hour */
  --sans: 'Geist', 'Geist Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  --bg:     light-dark(oklch(98.2% 0.004 95),      oklch(14% 0.006 260));
  --fg:     light-dark(oklch(19% 0.01 95),         oklch(93% 0.006 95));
  --muted:  light-dark(oklch(48% 0.012 95),        oklch(66% 0.01 95));
  --hair:   light-dark(oklch(19% 0.01 95 / 0.14),  oklch(93% 0.006 95 / 0.14));
  /* Lightness and chroma are fixed per theme, so contrast holds at every hue:
   * 5.9:1 on --bg in light, 9.8:1 in dark. */
  --accent:      light-dark(oklch(46% 0.12 var(--hue)),        oklch(80% 0.12 var(--hue)));
  --accent-soft: light-dark(oklch(46% 0.12 var(--hue) / 0.12), oklch(80% 0.12 var(--hue) / 0.14));
  --flock:       light-dark(oklch(42% 0.10 var(--hue)),        oklch(76% 0.10 var(--hue)));
  /* A shadow isn't a colour, so light-dark() goes on each of its two colours
   * rather than around the whole list — one definition either way. Dark needs
   * a heavier shadow to read at all against a near-black page. */
  --shadow:
    0 1px 2px      light-dark(oklch(0% 0 0 / 0.06), oklch(0% 0 0 / 0.3)),
    0 12px 40px -12px light-dark(oklch(0% 0 0 / 0.25), oklch(0% 0 0 / 0.7));

  /* The light of the hour (main.js: applyLight, from hue.js: lightAt). Written
   * here so the page still looks lit with no script at all — these defaults are
   * a mid-morning sun, which is where the wash sits before JS says otherwise. */
  --light-x: 22%; --light-y: 10%; --glow: 0.89;
  /* The wash's colour is the hour's own hue, so daylight is gold and the small
   * hours are blue without a second palette. --glow scales its alpha: the wash
   * is ambient light, and there is less of it at night.
   *
   * The THEME picks which light: a light page is daylit and takes the hour's
   * hue; a dark page is night, so its wash is moonlight — fixed at 260, the
   * dark --bg's own hue, nearly drained of chroma. The clock still moves both.
   * Tuned per theme, not symmetrically: the same wash multiplies a near-black
   * page and merely tints a near-white one. DESIGN.md, "Light". */
  --wash: light-dark(oklch(76% 0.15 var(--hue) / calc(var(--glow) * 0.17)),
                     oklch(68% 0.045 260 / calc(var(--glow) * 0.22)));

  /* The one theme-varying value that isn't a colour — see below. */
  --shot-filter: none;
  --shot-filter-night: brightness(.88);

  --gutter: clamp(1.25rem, 5vw, 5rem);
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  /* Two beats, shared by every interactive state (see "One interactive
   * grammar" below): things travel on --t-nudge, colour catches up on --t-tint.
   * Naming them is what keeps a dozen controls settling on the same rhythm. */
  --t-nudge: .45s var(--ease-out);
  --t-tint: .3s var(--ease-out);
  transition: --hue 2s linear;
}
/* The manual theme switch. Nothing to restate — the scheme is the palette.
 * Except --shot-filter, which is the one token light-dark() can't carry: it
 * isn't a colour, `filter` wants a keyword, and CSS has no
 * `@media (color-scheme: dark)` to key a non-colour off. So the *switch* is
 * written twice below; the *value* is still written once, above. */
:root[data-theme='dark']  { color-scheme: dark; --shot-filter: var(--shot-filter-night); }
:root[data-theme='light'] { color-scheme: light; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { --shot-filter: var(--shot-filter-night); }
}

/* --- Base ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
/* overflow-x lives HERE, not only on body, so that body's own value stops
 * propagating to the viewport and body clips its own children instead. That
 * matters for the phone canvas below: absolutely positioned and 120px wider
 * than the screen, it would otherwise widen the document and make a phone
 * shrink-to-fit the whole page (measured: a 390px viewport reporting 450). */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 400 1rem/1.5 var(--sans);
  font-feature-settings: 'ss01', 'cv11';    /* Geist: single-storey a, open digits */
  -webkit-font-smoothing: antialiased;
  hanging-punctuation: first last;
  overflow-x: clip;
  position: relative;   /* the containing block for the phone's hero canvas */
  transition: background-color .4s var(--ease-in-out), color .4s var(--ease-in-out);
}
::selection { background: var(--accent-soft); color: var(--fg); }

a { color: inherit; text-decoration-color: transparent; text-underline-offset: .18em; text-decoration-thickness: 1px; transition: text-decoration-color .3s var(--ease-out), color .3s var(--ease-out); }
a:hover, a:focus-visible { text-decoration: underline; text-decoration-color: var(--accent); color: var(--accent); }
/* With a pointer and script, the flock draws the underline first (main.js traces
 * it); the CSS one fades in just behind them, as if they left it there. */
@media (hover: hover) and (pointer: fine) {
  .js a:hover:not(:focus-visible) { transition: color .3s var(--ease-out), text-decoration-color .5s var(--ease-out) .7s; }
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
figure { margin: 0; }
img, svg { display: block; max-width: 100%; height: auto; }

/* --- One interactive grammar --------------------------------------------- */
/* Everything that answers a pointer does it in one of two ways, written here
 * once instead of reinvented per control:
 *
 *   NUDGE — a thing that leads somewhere (a link, an archive row) takes the
 *     accent colour and shifts a little the way it is pointing.
 *   DISC  — a round 44 px control (the sheet's x, its arrows, the theme dot)
 *     lifts from muted to fg on a soft accent disc.
 *
 * Both animate colour and transform only: no interactive state on this page
 * touches a layout property. The archive row used to animate `padding-left`,
 * which re-solved its four-column grid on every one of the 450 ms of frames —
 * indistinguishable to look at, and the only layout work left in a page that
 * otherwise never reflows after load. The nudge below moves the row's children
 * instead of its box, which is also why the li's full-width rule underneath
 * stays exactly where it is: that border belongs to the parent, and a
 * transform on a child cannot reach it.
 */
.sheet .close, .sheet-bar button {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--muted);
  transition: color var(--t-tint), background-color var(--t-tint);
}
.sheet .close:hover, .sheet .close:focus-visible,
.sheet-bar button:hover, .sheet-bar button:focus-visible {
  color: var(--fg);
  background: var(--accent-soft);
}

.mono { font-family: var(--mono); font-feature-settings: 'tnum'; font-size: .8125rem; letter-spacing: .01em; }
.skip { position: absolute; left: var(--gutter); top: -3rem; padding: .5rem .75rem; background: var(--bg); border: 1px solid var(--hair); z-index: 10; }
.skip:focus-visible { top: 1rem; }

/* --- The flock's canvas, and the still for when there's no script -------- */
/* The birds live in the VIEWPORT, not on the page: the canvas is fixed, and
 * the content scrolls through their world (the worker offsets it by the
 * scroll position). It bleeds 60px past every edge so a bird can leave the
 * visible page, turn around off-stage, and come back — no visible rebounds.
 * The bleed is pure off-screen paint the compositor still pays for, so it is
 * as small as the exit still reading naturally allows; with DPR ≤ 1.5 that
 * keeps the layer cheap.
 *
 * It sits ABOVE the page content (z-index higher than .hero/main/footer):
 * the content is a soft field the birds are nudged away from, not a wall,
 * so occasionally one drifts over the text — and since it renders on top,
 * that reads as a bird flying over the page, not as a glitch underneath
 * it. pointer-events stay none, so it's invisible to clicks and hover. */
#flock, .still {
  position: fixed;
  top: -60px; left: -60px;
  width: calc(100vw + 120px);
  height: calc(100svh + 120px);
  z-index: 2; pointer-events: none;
}
.still { color: var(--flock); }
/* On a phone the flock belongs to the HERO, not to the viewport. The archive
 * is full-width there — no margin, no whitespace — so a mark that followed you
 * down the page would have no choice but to sit ON the rows you are reading.
 * Anchored to the document instead, the canvas simply scrolls away with the
 * hero it came from, and the archive is clean. The layer still scrolls on the
 * compositor; it is the same box, just carried by the page instead of pinned
 * against it. main.js stops subtracting the scroll offset to match (the world
 * no longer moves), keyed off this very query. */
@media (max-width: 699px) {
  #flock, .still { position: absolute; }
}
/* The still yields to the canvas only once the flock is actually running —
 * `flock-on` is set by main.js AFTER the runner starts, not by the inline
 * `js` probe in index.html. Hiding it on `js` alone meant any failure in
 * main.js (a stale cached copy, a bad import, an unsupported feature) left
 * the fallback hidden and the canvas never started: an empty sky, no birds
 * either way. Gate it on the flock, and a broken main.js degrades to the
 * still instead of to nothing. */
.flock-on .still { display: none; }
.still polyline { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.25; }

/* --- The wash: sunlight on the page, moonlight after dark ---------------- */
/* One fixed bloom where the light is (--light-x/y, set by the clock). At
 * z-index 0 it is under every content layer and over the page background, so
 * --bg stays the colour contrast is measured against. It breathes on
 * `transform` and `opacity` ONLY — compositor properties, so the gradient
 * rasterises once and is never repainted. See DESIGN.md, "Light". */
body::before {
  content: '';
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  /* Flat to 30% of the radius before the falloff: a plain two-stop radial peaks
   * at one point and fades everywhere else, so with the source near the top
   * edge the page saw only the tail of it. */
  background: radial-gradient(72% 62% at var(--light-x) var(--light-y),
    var(--wash) 0%, var(--wash) 30%, transparent 78%);
}
/* Wrapped in no-preference rather than left to the global reduced-motion
 * override at the foot of this file: that override sets animation-duration to
 * .01ms, which does not stop an infinite animation — it restarts it forever. */
@media (prefers-reduced-motion: no-preference) {
  body::before { animation: wash-breathe 45s var(--ease-in-out) infinite; }
}
@keyframes wash-breathe { 50% { transform: scale(1.05) translateY(1.5%); opacity: .9; } }

/* Content sits below the flock but above the page background. */
.hero, main, .site-footer { position: relative; z-index: 1; pointer-events: none; }
.hero *, main *, .site-footer * { pointer-events: auto; }

/* --- Hero ---------------------------------------------------------------- */
.hero {
  min-height: 100svh;
  min-height: 100dvh;
  display: grid;
  align-content: end;
  padding: var(--gutter);
  padding-bottom: calc(var(--gutter) + env(safe-area-inset-bottom));
  padding-left: calc(var(--gutter) + env(safe-area-inset-left));
}
.hero-text { max-width: 38rem; }
h1 {
  font-size: clamp(1.625rem, 1.2rem + 1.8vw, 2.5rem);
  font-weight: 500;
  letter-spacing: -0.022em;
  line-height: 1.1;
  margin: 0 0 .75em;
  margin-left: -0.06em;             /* optical alignment of the J's top bar */
  text-wrap: balance;
}
.hero p {
  margin: 0;
  font-size: clamp(1rem, .9rem + .5vw, 1.25rem);
  line-height: 1.45;
  color: var(--fg);
  text-wrap: pretty;
  max-width: 34ch;
}
.hero-links { display: flex; gap: 1.5rem; margin-top: 1.75rem; list-style: none; padding: 0; font-family: var(--mono); font-size: .8125rem; letter-spacing: .02em; text-transform: lowercase; }
.hero-links a { display: inline-block; padding: .25rem 0; min-height: 44px; line-height: 44px; }
.hero-links li:last-child { margin-left: auto; }
/* The arrow keeps its own slow bob, pointing the way down the page. It rides on
 * `translate` while the hover nudge stays on `transform`, because the two
 * compose — otherwise the running animation would simply outrank the hover. */
.hero-links a[href='#archive']::after {
  content: '↓'; display: inline-block; margin-left: .45em;
  transition: transform .4s var(--ease-out);
}
.hero-links a[href='#archive']:hover::after { transform: translateY(3px); }
@media (prefers-reduced-motion: no-preference) {
  .hero-links a[href='#archive']::after { animation: archive-bob 2.6s var(--ease-in-out) infinite; }
}
@keyframes archive-bob { 50% { translate: 0 4px; } }

/* As you scroll away, the hero text recedes; the flock doesn't.
 *
 * The range is a PERCENTAGE of the scrollable distance, not a vh figure. This
 * page is short — a 100dvh hero over an archive of six — so on a tall window
 * the whole document scrolls less than one viewport height, and the hero can
 * never leave the screen at all. Keyed to `70vh` the fade then never finished:
 * at 1400px tall it stalled at opacity .31 with the github/linkedin/archive
 * links still sitting there while you read the archive. A percentage is
 * reachable by construction, whatever the window and however long the page.
 *
 * It ends at 0, not at a ghost — and at `visibility: hidden`, which is the
 * part that matters. Opacity alone would leave three invisible links in the
 * tab order, and you cannot rely on focus scrolling them back into view: on
 * exactly the tall windows where this goes wrong they are already ON screen,
 * so the browser has nothing to scroll. visibility takes them out of the tab
 * order outright, and it interpolates as `visible` until the very end, so the
 * fade still plays in full. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-text { animation: hero-away linear both; animation-timeline: scroll(); animation-range: 0 55%; }
    @keyframes hero-away { to { opacity: 0; transform: translateY(-2rem); visibility: hidden; } }
  }
}

/* --- Archive ------------------------------------------------------------- */
.archive { padding: calc(var(--gutter) * 1.5) var(--gutter) var(--gutter); max-width: 60rem; }
.archive header { display: flex; align-items: baseline; gap: 1.25rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
h2 { font-size: 1rem; font-weight: 500; margin: 0; letter-spacing: -0.01em; }
.meta { margin: 0; color: var(--muted); }
/* The row numbers are COUNTED, not typed. They were six hardcoded spans, and a
 * reorder desynced them from the rows they label — which has happened. Each
 * <ol class="rows"> resets its own counter, so Making and Archive both start
 * at 01. The span stays: it holds the grid's first column and the aria-hidden
 * (the number is decoration — the name is the accessible label). */
.rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--hair); counter-reset: rows; }
.rows li { counter-increment: rows; }
.row .num::before { content: counter(rows, decimal-leading-zero); }
.rows li { border-bottom: 1px solid var(--hair); }
/* Each row lifts the last few pixels into place as it arrives, on its OWN
 * timeline (`view()`) — no delay chain to fall out of step with the scroll.
 * It moves the .row, not the li: the li carries the hairline, and a rule that
 * shifted while its neighbour's didn't would break the list's rhythm. Nothing
 * here is a layout property. No opacity — see DESIGN.md: the metadata columns
 * are quiet greys with 13% of headroom over AA, and a fade spends it. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rows .row { animation: row-in linear both; animation-timeline: view(); animation-range: cover 0% cover 20%; }
    @keyframes row-in { from { translate: 0 8px; } }
  }
}
.row {
  display: grid;
  grid-template-columns: 2.25rem 1fr auto 1.5rem;
  align-items: baseline;
  gap: 1rem;
  min-height: 56px;
  padding: 1rem 0;
  text-decoration: none !important;
  color: inherit !important;
}
.row .num, .row .medium { font-family: var(--mono); font-feature-settings: 'tnum'; font-size: .8125rem; color: var(--muted); }
.row .name { font-size: 1.125rem; font-weight: 450; letter-spacing: -0.012em; }
.row .arrow { font-family: var(--mono); color: var(--muted); text-align: right; }
/* NUDGE. The number and the name travel; the medium and the arrow are anchored
 * to the right-hand edge and stay put (which is precisely what padding-left
 * did — the 1fr column absorbed the shift, so only the first two moved). */
.row .num, .row .name, .row .medium, .row .arrow { transition: transform var(--t-nudge), color var(--t-tint); }
.row:hover .num, .row:focus-visible .num,
.row:hover .name, .row:focus-visible .name { transform: translateX(.5rem); }
.row:hover .name, .row:focus-visible .name { color: var(--accent); }
.row:hover .arrow, .row:focus-visible .arrow { transform: translateX(4px); color: var(--accent); }
@media (max-width: 30rem) {
  .row { grid-template-columns: 1.75rem 1fr 1.25rem; }
  .row .medium { grid-column: 2; grid-row: 2; margin-top: -.35rem; }
  /* Stacked under the name in the same column, so here it travels with it. */
  .row:hover .medium, .row:focus-visible .medium { transform: translateX(.5rem); }
}

/* --- Sheets (full-length screenshots) ----------------------------------- */
/* Without script: :target turns the sheet into a full-screen overlay.
 * With script: the same node is moved into <dialog> (focus trap, ESC, inert). */
.sheet { display: none; }
html:not(.js) .sheet:target, dialog[open] .sheet { display: block; }
html:not(.js) .sheet:target {
  position: fixed; inset: 0; z-index: 20; overflow: auto; overscroll-behavior: contain;
  background: var(--bg); padding: 3.5rem var(--gutter) var(--gutter);
}
.sheet .close {                                    /* DISC, plus its corner */
  position: absolute; top: .75rem; right: .75rem;
  font-family: var(--mono); font-size: 1.25rem;
  text-decoration: none !important;
}
/* The shadow belongs to the IMAGE, not the figure. Putting it on the figure
 * wrapped the caption in the same raised card, so the label read as a white
 * strip welded to the screenshot and floated with it. The screenshot hovers;
 * the caption lies flat on the sheet, spaced off the image it names. */
.sheet figure { max-width: 960px; margin: 0 auto; }
.sheet figure + figure { margin-top: 3.25rem; }
.sheet figcaption { font-family: var(--mono); font-size: .75rem; color: var(--muted); margin-top: 1rem; }
/* These are 2013 screenshots: full-bleed white pages. At their own brightness
 * on a 14%-lightness sheet, opening one at night is a flashbulb. A gentle dim
 * takes the glare off without washing the design out — and it stays `none` in
 * light mode, so the light path pays nothing for a filter it doesn't want. */
.sheet img { display: block; width: 100%; background: #fff; box-shadow: var(--shadow); filter: var(--shot-filter); }
.sheet .ruler {
  display: none; position: sticky; top: 50%; float: right; margin: 0 -2.75rem 0 0; width: 0; height: 0;
  font-family: var(--mono); font-size: .6875rem; color: var(--muted); white-space: nowrap;
  writing-mode: vertical-rl; transform: translateY(-50%);
}
@media (min-width: 1160px) { .sheet .ruler { display: block; } }

dialog#sheet {
  --sheet-top: 2.5rem;
  width: min(100vw, 960px + 2 * var(--gutter)); max-width: none;
  height: calc(100dvh - var(--sheet-top)); max-height: none; margin: var(--sheet-top) auto 0;
  padding: 3.5rem var(--gutter) var(--gutter);
  border: 0; border-radius: 6px 6px 0 0; background: var(--bg); color: var(--fg); overscroll-behavior: contain;
  box-shadow: var(--shadow);
}
@media (max-width: 700px) { dialog#sheet { --sheet-top: 0px; border-radius: 0; } }
/* No backdrop paint: the page itself recedes, so the flock stays visible and a
 * few birds can perch on the sheet's top edge. */
dialog#sheet::backdrop { background: transparent; }
.hero, main, .site-footer { transition: opacity .4s var(--ease-out); }
.sheet-open .hero, .sheet-open main, .sheet-open .site-footer { opacity: .22; }
dialog#sheet[open] { animation: sheet-in .45s var(--ease-out); }
@keyframes sheet-in { from { opacity: 0; transform: translateY(12px); } }
.sheet-bar {
  position: sticky; top: -3.5rem; margin: -3.5rem calc(-1 * var(--gutter)) 0; padding: .5rem var(--gutter);
  display: flex; align-items: center; gap: .5rem; min-height: 3.5rem; z-index: 2;
  background: color-mix(in oklab, var(--bg) 94%, transparent);
  font-family: var(--mono); font-size: .8125rem; color: var(--muted);
}
.sheet-bar .title { color: var(--fg); margin-right: auto; }
dialog#sheet .sheet { margin-top: 1rem; }
dialog#sheet .sheet .close { display: none; }

/* --- Footer -------------------------------------------------------------- */
.site-footer { padding: calc(var(--gutter) * 1.5) var(--gutter) calc(var(--gutter) + env(safe-area-inset-bottom)); font-family: var(--mono); font-size: .8125rem; color: var(--muted); }
.site-footer p { margin: 0; display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; align-items: center; }
.site-footer .arrow { display: inline-block; opacity: 0; transition: opacity .8s var(--ease-out); }
.site-footer .arrow.drawn, html:not(.js) .site-footer .arrow { opacity: 1; }
/* The theme control. It used to be a single `·`, and a 44px hit box drawn
 * around it with a -1rem margin: the target was technically there, but nothing
 * about a dot says "theme", so people read the glyph as the button and aimed
 * at 4px of it. It now shows its own surface — a hairline pill you can see the
 * edges of — with an icon for recognition and the current mode in a word,
 * because there are THREE states and an icon alone would be a guess. The word
 * is the visible label and the aria-label contains it, so the accessible name
 * and what you can read still agree. */
.site-footer .theme {
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 44px; padding: 0 .9rem 0 .75rem;
  font: inherit; color: var(--muted);
  border: 1px solid var(--hair); border-radius: 999px;
  transition: color var(--t-tint), background-color var(--t-tint), border-color var(--t-tint);
}
.site-footer .theme:hover, .site-footer .theme:focus-visible {
  color: var(--fg); background: var(--accent-soft); border-color: transparent;
}
.theme-icon { width: 15px; height: 15px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }
.i-auto path, .i-dark path { fill: currentColor; stroke: none; }   /* the lit half, and the moon */
/* One state at a time; `:root` has no data-theme until you choose one. */
.theme-icon > g { display: none; }
:root:not([data-theme]) .i-auto,
:root[data-theme='light'] .i-light,
:root[data-theme='dark'] .i-dark { display: inline; }
/* With no script it cannot switch anything, so it does not pretend to. */
html:not(.js) .site-footer .theme { display: none; }

/* Prev/next swaps one project's figures for another's inside the same open
 * dialog. Naming only that area means the crossfade is the figures changing,
 * not the page turning over: the bar, the backdrop and the flock behind it all
 * hold still, which is why the root snapshot's own animation is switched off
 * (the `isolation`/`display` pair is what makes turning it off actually paint).
 * Opening a sheet starts no transition at all — only step() does. */
@media (prefers-reduced-motion: no-preference) {
  #sheet > .sheet { view-transition-name: sheet-figures; }
  ::view-transition-image-pair(root) { isolation: auto; }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; display: block; }
  ::view-transition-old(sheet-figures), ::view-transition-new(sheet-figures) { animation-duration: .2s; }
}

/* --- Motion & print ------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; scroll-behavior: auto !important; }
}
@media print { #flock, .still, .site-footer .theme, body::before { display: none; } .sheet { display: block; } }
