/* ===== DESIGN TOKENS ===== */
:root {
  /* Typography */
  /* One face for the whole page; weight, size and colour do the rest. */
  --font-ui: 'Literata', 'Georgia', serif;
  --font-reading: 'Literata', 'Georgia', serif;

  /* Font Sizes */
  --text-2xs: 10px;
  --text-xs: 12px;
  --text-sm: 14px;
  --text-base: 16px;
  --text-lg: 20px;
  --text-xl: 24px;

  /* Font Weights */
  --font-light: 300;
  --font-regular: 400;
  --font-medium: 500;
  --font-bold: 700;

  /* Line Heights */
  --leading-none: 1;
  --leading-tight: 1.25;
  --leading-snug: 1.375;
  --leading-normal: 1.5;
  --leading-relaxed: 1.625;
  --leading-loose: 2;
  /* The Times' headline leading, 46px on 40px, tighter than tight. */
  --leading-headline: 1.15;

  /* Letter Spacing */
  --tracking-tight: -0.025em;
  --tracking-wide: 0.025em;
  --tracking-wider: 0.05em;
  --tracking-widest: 0.1em;

  /* Type Scale (typeScale in the tokens): h1, and an article's headline and
     the summary under it, grow with the screen */
  --h1-size: 28px;
  --headline-size: 34px;
  --summary-size: 20px;
  --h2-size: 24px;
  --h3-size: 18px;
  --h4-size: 18px;
  --body-size: 18px;

  /* Spacing */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;

  /* Vertical rhythm between page sections. Phones get the tighter of the two
     spacing steps so the first screen carries content instead of air. */
  --section-gap: var(--space-lg);

  /* Layout */
  --container-reading: 680px;
  --container-lg: 1024px;
  --container-xl: 1280px;
  /* The width the side nav claims at the left edge on a wide screen, for pages
     whose content runs wider than the reading column and has to start past it. */
  --nav-column: 200px;

  /* Border Radius */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-full: 9999px;

  /* Border */
  --border-thin: 1px;
  --border-medium: 2px;

  /* Animation Duration */
  --duration-fast: 100ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --duration-slower: 500ms;
  --duration-cinematic: 1200ms;
  --duration-intro-word: 200ms;
  --duration-intro-curtain: 1500ms;
  --duration-intro-hold: 1100ms;
  --duration-intro-resolve: 600ms;
  --duration-intro-arrive: 1500ms;
  --duration-intro-chrome: 1000ms;
  --delay-intro-chrome: 750ms;
  --duration-logo-draw: 6000ms;        /* the pen's travel through S, 8 and R */
  --duration-logo-ink-spread: 1600ms;  /* fresh ink reaching full width behind it */
  --duration-theme-toggle: 400ms;      /* the sun turning into the moon and back */
  --ease-arrive: cubic-bezier(.33, 0, .2, 1);
  --intro-derez-scale: 28;
  --z-curtain: 900;   /* above --z-header (300) so the curtain covers it */
  --z-header: 300;

  /* Animation Easing */
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
  --ease-curtain: cubic-bezier(0.76, 0, 0.16, 1);
  --ease-glide: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-pen: cubic-bezier(0.22, 0.03, 0.35, 0.9);  /* eases in, rushes the middle, settles */
  --ease-spring-back: cubic-bezier(0.34, 3.85, 0.64, 1);  /* overshoots well past rest, settles */

  /* The byline and company circle, everywhere it is drawn (markSize in the tokens). */
  --co-mark-size: 44px;

  /* An essay's Listen player, in the Times' proportions (listen in the tokens):
     the seek bar's track and dot, and the play and pause glyphs. */
  --listen-track-height: 3px;
  --listen-dot-size: 12px;
  --listen-icon-size: 18px;

  /* Avatar stack: how a bubble rises to the pointer, in a Press byline, the
     social links, the career strip or on Work (avatarStack in the tokens).
     main.js reads the three amounts; the stylesheet runs the timing. */
  --duration-avatar: 320ms;
  --avatar-lift: -4px;
  --avatar-scale: 1.05;
  --avatar-falloff: 0.45;
  --ease-avatar-in: var(--ease-glide);
  --ease-avatar-out: var(--ease-spring-back);

  /* Title link: how a title in Press, Projects and Writing answers the
     pointer, as marcbaselga.com's project names do (titleLink and animation in
     the tokens). An underline draws in under the words on --ease-draw, quick
     off the mark with a long settle, and the arrow after a project or an
     essay brightens from its resting opacity and nudges up and out, turning a
     little. */
  --duration-underline-draw: 600ms;
  --ease-draw: cubic-bezier(0.165, 0.84, 0.44, 1);
  --ease-nudge: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --title-link-arrow-size: 0.75em;
  --title-link-arrow-gap: 0.2em;
  --title-link-arrow-rest: 0.5;
  --title-link-arrow-shift: 2px;
  --title-link-arrow-tilt: 5deg;

  /* Blur-up: how far out of focus the portrait's inline thumbnail and the
     photo start, and how much larger the photo starts (blurUp in the tokens). */
  --blur-up-blur: 42px;
  --blur-up-scale: 1.5;

  /* Opacity */
  --opacity-faint: 0.32;
  --opacity-muted: 0.6;
  --opacity-subtle: 0.8;

  /* Focus Ring */
  --focus-width: 2px;
  --focus-offset: 2px;

  /* Icon Sizes */
  --icon-sm: 16px;
  --icon-md: 20px;
  --icon-lg: 24px;

  /* Brand */
  /* Gmail's five colours, the same in both modes, as the logo is. */
  --brand-gmail-blue: #4285F4;
  --brand-gmail-red: #EA4335;
  --brand-gmail-dark-red: #C5221F;
  --brand-gmail-yellow: #FBBC04;
  --brand-gmail-green: #34A853;

  /* Floating Chrome */
  /* A capsule is 56px tall with 16px of padding either side of its contents,
     so a lone 24px glyph makes a box exactly 56px wide: the circle for one
     icon and the pill for a label come out of the same two numbers. */
  --chrome-size: 56px;
  --chrome-gap: 16px;
  --chrome-inset: 24px;
  /* Where the bottom corners rest: the inset, plus the step above an iPhone's
     home indicator. The safe-area inset is zero unless the page opts into the
     full screen. Every control pinned to the bottom edge uses this one floor,
     so the corners cannot drift apart. */
  --chrome-floor: calc(var(--chrome-inset) + env(safe-area-inset-bottom, 0px));
  --chrome-rise: 8px;
  --chrome-wash: 120px;
  --chrome-blur: blur(24px) saturate(1.2);

  /* Over a video the words are white in both themes, on a shade that darkens
     the frame's foot. The strip's dwell is how long a phone shows one talk
     before moving on. */
  --media-text: #FFFFFF;
  --media-text-muted: rgba(255, 255, 255, 0.72);
  --media-edge: rgba(255, 255, 255, 0.35);
  --media-shade: rgba(0, 0, 0, 0.55);
  --featured-dwell: 6000ms;

  /* Now Playing */
  /* A slipcase on the shelf is --case-w wide by its height at the poster
     ratio, --case-depth thick, turned --case-tilt so its spine shows, seen
     from --case-perspective away. That is the newest case at rest, and the
     case under the pointer; every other case is a share of it, no less than
     --case-scale-min, and turns in the same share. Each case tucks behind
     the one before by --case-tuck of the newest case's width and
     --case-tuck-more of its own size besides, so a small case hides deep
     and a case that grows comes out from under its neighbour, as on rog.ie's
     shelf. --case-eject is how much of its width the tape slides out of the
     case under the pointer. On a phone the newest case is --case-share of
     the column, which fits the whole fan on every phone, and no narrower
     than keeps the smallest case a tap target. */
  --case-share: 0.3;
  --case-scale-min: 0.6;
  --case-w: max(calc((100vw - 4 * var(--space-md)) * var(--case-share)),
                calc(var(--space-2xl) / var(--case-scale-min)));
  --case-depth: 26px;
  --case-tilt: -20deg;
  --case-perspective: 1000px;
  --case-tuck: 0.05;
  --case-tuck-more: 0.2;
  --case-eject: 0.25;
  --aspect-poster: 2 / 3;

  /* Haze Chaser */
  /* Split-flap counters and red compass marks. */
  --hz-compass: #D6432F;
  --hz-card-frame: #0A0A0A;
  --hz-card-edge: rgba(212, 212, 212, 0.55);
  --hz-card-shadow: rgba(0, 0, 0, 0.40);
  --hz-card-upper-top: #262626;
  --hz-card-upper-bottom: #1A1A1A;
  --hz-card-lower-top: #0A0A0A;
  --hz-card-lower-bottom: #1A1A1A;
  --hz-digit: #F5F5F5;
  --hz-night-edge: rgba(214, 67, 47, 0.30);
  --hz-night-light: #D6432F;
  --hz-night-glow: rgba(214, 67, 47, 0.28);
  --hz-night-glow-large: rgba(214, 67, 47, 0.22);
}

/* Tablet Breakpoint (768px+) */
@media (min-width: 768px) {
  :root {
    --chrome-inset: 32px;
    --section-gap: var(--space-xl);
    --case-w: 140px;
    --h1-size: 32px;
    --headline-size: 42px;
    --summary-size: 24px;
  }
}

/* Desktop Breakpoint (1024px+) */
@media (min-width: 1024px) {
  :root {
    --h1-size: 36px;
  }
}

/* ===== DARK MODE (Default) ===== */
html.dark {
  --color-bg: #0A0A0A;
  --color-bg-elevated: #1A1A1A;
  --color-text: #FFFFFF;
  --color-text-muted: #A3A3A3;
  --color-text-subtle: #D4D4D4;
  --color-text-body: #F5F5F5;
  --color-link: #8CB4D9;
  --color-link-hover: #B3D1EE;
  --color-border: #404040;
  --color-border-muted: #262626;
  --focus-color: #FFFFFF;
  --map-ink: #FFFFFF;
  --map-grid: rgba(255, 255, 255, 0.045);
  --map-land: rgba(255, 255, 255, 0.022);
  --map-edge: rgba(255, 255, 255, 0.12);
  --map-visited-land: rgba(255, 255, 255, 0.05);
  --map-visited-edge: rgba(255, 255, 255, 0.26);
  --map-active-land: rgba(255, 255, 255, 0.085);
  --map-active-edge: #FFFFFF;
  --map-halo: rgba(255, 255, 255, 0.22);
  --globe-land-lit: #FFFFFF;
  --globe-land-mid: #D4D4D4;
  --globe-land-dark: #404040;
  --globe-ocean-lit: #262626;
  --globe-ocean-dark: #0A0A0A;

  --chrome-fill: rgba(64, 64, 64, 0.65);
  --chrome-edge: rgba(255, 255, 255, 0.10);
  --chrome-shadow: 0 4px 12px rgba(0, 0, 0, 0.40);
  --chrome-glyph: #FFFFFF;
  --chrome-wash-end: rgba(10, 10, 10, 0);

  /* The tape in a slipcase is a photograph; its edge, seen in the sleeve's
     open side, is black plastic in both modes, and the title on a coloured
     spine is white in both. */
  --tape-edge: #262626;
  --case-ink: #FFFFFF;

  /* LinkedIn's own blue, #0A66C2, falls under 3:1 on dark glass, so dark
     mode takes a lighter step of the same blue. */
  --brand-linkedin: #378FE9;

}

/* ===== LIGHT MODE ===== */
html.light {
  --color-bg: #FFFFFF;
  --color-bg-elevated: #F5F5F5;
  --color-text: #0A0A0A;
  --color-text-muted: #404040;
  --color-text-subtle: #262626;
  --color-text-body: #262626;
  --color-link: #326891;
  --color-link-hover: #1F4E73;
  --color-border: #E5E5E5;
  --color-border-muted: #F5F5F5;
  --focus-color: #000000;
  --map-ink: #171717;
  --map-grid: rgba(0, 0, 0, 0.05);
  --map-land: rgba(0, 0, 0, 0.03);
  --map-edge: rgba(0, 0, 0, 0.14);
  --map-visited-land: rgba(0, 0, 0, 0.06);
  --map-visited-edge: rgba(0, 0, 0, 0.3);
  --map-active-land: rgba(0, 0, 0, 0.1);
  --map-active-edge: #171717;
  --map-halo: rgba(0, 0, 0, 0.22);
  --globe-land-lit: #171717;
  --globe-land-mid: #404040;
  --globe-land-dark: #D4D4D4;
  --globe-ocean-lit: #E5E5E5;
  --globe-ocean-dark: #FFFFFF;

  --chrome-fill: rgba(255, 255, 255, 0.72);
  --chrome-edge: rgba(0, 0, 0, 0.04);
  --chrome-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
  --chrome-glyph: #262626;
  --chrome-wash-end: rgba(255, 255, 255, 0);

  --tape-edge: #262626;
  --case-ink: #FFFFFF;

  --brand-linkedin: #0A66C2;

}

/* ===== CURSOR ===== */
/* A small translucent disc, the way Chrome's device mode shows a finger over a
   page, set once on html. Links deliberately keep the hand the browser gives
   them, and buttons ask for it, since a browser gives them the arrow. The
   fill mirrors --color-text by hand, because a data
   URI cannot read a custom property. The hotspot is the disc's centre. */
@media (pointer: fine) {
  html {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23FFFFFF' fill-opacity='0.4'/%3E%3C/svg%3E") 10 10, auto;
  }

  html.light {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='10' fill='%230A0A0A' fill-opacity='0.35'/%3E%3C/svg%3E") 10 10, auto;
  }
}

/* A forced-colours reader picked their own palette and pointer size. Give it back. */
@media (forced-colors: active) {
  html {
    cursor: auto;
  }
}

/* ===== BASE STYLES ===== */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: 100px;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  font-family: var(--font-ui);
  font-size: var(--body-size);
  line-height: var(--leading-normal);
  color: var(--color-text);
  background-color: var(--color-bg);
  transition: background-color var(--duration-slow) var(--ease-in-out),
              color var(--duration-slow) var(--ease-in-out);
}

/* ===== LAYOUT ===== */
.container {
  max-width: var(--container-reading);
  margin: 0 auto;
  padding: var(--space-lg) var(--space-md);
}

@media (min-width: 768px) {
  .container {
    padding: var(--space-2xl) var(--space-lg);
  }
}

/* ===== FLOATING HEADER ===== */
/* The overlay is fixed, it does not scroll, and it is meant to cover the page.
   Letters run behind it and come out the other side partly obscured; nothing
   reflows and nothing is padded out of the way to make room. */
.header-wrapper {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-header);
  pointer-events: none;
}

/* The top of the window fades to the page's own colour over 120px, so a line
   of text is already washed out by the time it reaches the chrome. A scrim
   rather than a mask on the scroller: a mask or a filter on any ancestor of a
   fixed element makes that ancestor its containing block, which would pin the
   chrome to the page instead of the window. Over a flat background the two
   read the same. */
.header-wrapper::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--chrome-wash);
  background: linear-gradient(to bottom, var(--color-bg), var(--chrome-wash-end));
}

/* The bar is the brand on the left and, on a narrow screen, the menu button on
   the right. It has no surface of its own: each control carries one. */
.floating-header {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--chrome-inset);
  pointer-events: auto;
  user-select: none;
}

/* ===== CHROME CAPSULES =====
   Every icon in the overlay wears the same capsule. Fully rounded, 56px tall,
   16px of padding either side of the glyph, so a 24px glyph comes out a
   circle without the circle being asked for by name. The wordmark is not an
   icon and wears nothing. */
.header-action-btn,
.scroll-cue,
.gallery-viewer-btn,
.featured-card-controls button,
.featured-card-controls a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--chrome-gap);
  min-width: var(--chrome-size);
  height: var(--chrome-size);
  padding: 0 var(--chrome-gap);
  border: 0;
  border-radius: var(--radius-full);
  cursor: pointer;
}
/* Glass, and the one surface every mark on the site shares, over a blur of
   whatever is behind it. In dark mode it is smoked: a dark grey tint at two
   thirds, a shade above the page and darker than a photo, under a white
   glyph. In light mode it lifts toward white under a dark glyph. The edge is
   an inset shadow rather than a border so it cannot add to the size the box
   was given.

   Two layers wear it. The two overlay controls, and the photography
   viewer's, float over the page and never scroll. The marks in the second list sit in the text, at the text's own
   level, and scroll with it; they borrow the look and none of the height. The
   selected Where Is card is the one surface larger than a mark to wear it,
   and the Where Is city markers wear it over the map, as Haze Chaser's
   capsules and the credit's list do over its map. */
.header-action-btn,
.scroll-cue,
.gallery-viewer-btn,
.featured-card-controls button,
.featured-card-controls a,
.hero-social-link,
.globe-btn,
.hz-control-group,
.co-mark,
.wi-trip.is-active .wi-card,
.wi-marker,
.hz-nav::before,
.hz-credit::before {
  background: var(--chrome-fill);
  box-shadow: var(--chrome-shadow),
              inset 0 0 0 var(--border-thin) var(--chrome-edge);
  backdrop-filter: var(--chrome-blur);
  -webkit-backdrop-filter: var(--chrome-blur);
  color: var(--chrome-glyph);
}

.header-logo {
  position: relative;  /* anchors the drawn copy of the mark, js/logo-draw.js */
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-family: var(--font-reading);
  font-size: var(--h1-size);
  font-weight: var(--font-bold);
  color: var(--color-text);
  text-decoration: none;
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
}

.header-logo-mark {
  font-style: normal;
}

/* While the pen writes the mark in, the text stays in place and keeps its name for
   screen readers; it is only unpainted, and a drawn copy sits over it. */
html[data-logo="drawing"] .header-logo-mark {
  color: transparent;
}

/* Glyphs are monoline and 24px, and rest a step below full strength, coming
   up to meet the pointer. The capsule's own opacity is left alone: the
   entrance animates it, and the cue drives it. */
.header-action-btn svg,
.gallery-viewer-btn svg,
.scroll-cue-arrow,
.globe-btn svg,
.co-mark svg {
  opacity: var(--opacity-subtle);
}

.header-action-btn svg,
.gallery-viewer-btn svg {
  width: var(--icon-lg);
  height: var(--icon-lg);
  transition: opacity var(--duration-normal) var(--ease-in-out);
}

.header-action-btn:hover svg,
.gallery-viewer-btn:hover svg,
.scroll-cue:hover .scroll-cue-arrow,
.hero-social-link:hover svg,
.globe-btn:hover svg,
#work-rows .bubbles:hover svg {
  opacity: 1;
}

/* A press is the whole capsule giving way. scale rather than transform,
   because the cue spends its transform on centring itself and the two
   compose where one property would not. */
.header-action-btn,
.scroll-cue,
.gallery-viewer-btn,
.globe-btn {
  transition: scale var(--duration-fast) var(--ease-out);
}

.header-action-btn:active,
.scroll-cue:active,
.gallery-viewer-btn:active,
.hero-social-link:active,
.globe-btn:active,
.essay-listen-play:active {
  scale: 0.96;
}

.bubble:focus-visible,
.globe-btn:focus-visible,
.essay-listen-play:focus-visible,
.essay-listen-seek:focus-visible,
.gallery-viewer-btn:focus-visible,
.header-action-btn:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* The overlay arrives after the page has settled: three quarters of a second
   of stillness, then one glide up together. The cue is the exception. JS
   decides whether there is anything below worth pointing at, so it owns its
   own opacity and shares only the rise. */
.header-logo,
.header-action-btn {
  animation: chrome-enter var(--duration-intro-chrome) var(--ease-glide)
             var(--delay-intro-chrome) both;
}

.scroll-cue {
  animation: chrome-rise var(--duration-intro-chrome) var(--ease-glide)
             var(--delay-intro-chrome) both;
}

@keyframes chrome-enter {
  from { opacity: 0; translate: 0 var(--chrome-rise); }
  to   { opacity: 1; translate: 0 0; }
}

@keyframes chrome-rise {
  from { translate: 0 var(--chrome-rise); }
  to   { translate: 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .header-logo,
  .header-action-btn,
  .scroll-cue {
    animation: none;
  }
}

/* The links stand in a column at the left edge, level with the first line of
   the page. The one whose section sits under the middle of the window is lit;
   the rest are muted until the pointer reaches them. The theme control closes
   the column a step below the last link, in the links' colour rather than a
   capsule of its own, so it reads as one of them. */
.side-nav {
  position: fixed;
  top: 220px;
  left: var(--chrome-inset);
  z-index: 0;
  display: flex;
  flex-direction: column;
  pointer-events: auto;
  user-select: none;
}

.side-nav a,
.side-nav-theme {
  padding: 1px 0;
  font-size: var(--body-size);
  line-height: var(--leading-snug);
  color: var(--color-text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration-normal) var(--ease-in-out);
}

.side-nav a:hover,
.side-nav a.active,
.side-nav-theme:hover {
  color: var(--color-text);
}

.side-nav a:focus-visible,
.side-nav-theme:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* The glyph is smaller than a finger or even a pointer needs, so the box is at
   least the 24px a target has to be, with the glyph at its left edge. A
   browser gives a button the arrow, not the hand a link gets, so it is asked
   for. */
.side-nav-theme {
  display: flex;
  align-items: center;
  align-self: flex-start;
  min-width: var(--icon-lg);
  min-height: var(--icon-lg);
  margin-top: var(--space-sm);
  border: 0;
  background: none;
  cursor: pointer;
}

.side-nav-theme svg {
  width: var(--icon-md);
  height: var(--icon-md);
}

/* Sun in light mode, moon in dark, and the one becomes the other: the rays draw
   in to the centre as the disc swells and a bite closes over its corner,
   leaving a crescent. Coming back, the bite lets go first and the rays follow.
   After the Classic toggle at toggles.dev, by Alfie Jones (MIT licence). The
   numbers are places in the glyph's 24-unit square, not design values.

   The bite is a clip path whose curved edge waits outside the square and moves
   in over the disc. Changing a path's d is newer than everything else here, so
   a browser without it slides the same shape into place instead. */
.theme-glyph-disc,
.theme-glyph-rays {
  transform-origin: center;
}

.theme-glyph-disc {
  transition: transform var(--duration-theme-toggle) var(--ease-in-out);
}

.theme-glyph-rays {
  transition: transform var(--duration-theme-toggle) var(--ease-in-out),
              opacity var(--duration-theme-toggle) var(--ease-in-out);
  transition-delay: calc(var(--duration-theme-toggle) * 0.15);
}

.theme-glyph-bite {
  transition: d var(--duration-theme-toggle) var(--ease-in-out),
              translate var(--duration-theme-toggle) var(--ease-in-out);
}

html.dark .theme-glyph-disc {
  transform: scale(1.7);
}

html.dark .theme-glyph-rays {
  transform: scale(0);
  opacity: 0;
  transition-delay: 0s;
}

html.dark .theme-glyph-bite {
  d: path("M0 2h13a1 1 0 0 0 10 10v14H0Z");
  transition-delay: calc(var(--duration-theme-toggle) * 0.15);
}

@supports not (d: path("M0 0")) {
  html.dark .theme-glyph-bite {
    translate: -13px 2px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .theme-glyph * {
    transition: none;
  }
}

/* Two lines that fold into a cross when the menu is open: a long one over a
   short one, both centred, the short one five eighths of the long. They are
   the one glyph that does not rest below full strength: lines this thin at
   80% read as grey on the glass, and the whole point of the disc is two crisp
   marks. Short and a little apart, so they sit in the circle rather than
   span it. The short line is the long one scaled down, not a narrower box,
   so it grows back to full length as it turns and the cross comes out even;
   the viewer's close button, a cross from the start, gets the full length
   for free. */
.header-menu-btn {
  display: none;
  position: relative;
  color: var(--chrome-glyph);
}

.menu-line {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 2px;
  margin: -1px 0 0 -11px;
  border-radius: var(--radius-full);
  background: currentColor;
  transition: transform var(--duration-fast) ease-in;
}

.menu-line:first-child { transform: translateY(-5px); }
.menu-line:last-child { transform: translateY(5px) scaleX(0.625); }

html.has-menu-open .menu-line {
  transition: transform var(--duration-fast) ease-out var(--duration-fast);
}

html.has-menu-open .menu-line:first-child { transform: rotate(45deg); }
html.has-menu-open .menu-line:last-child { transform: rotate(-45deg); }

/* Every control is something a thumb has to land on. The capsules clear the
   44px minimum at 56px on their own; the wordmark and the nav links are
   lettering, and need the floor. */
@media (pointer: coarse) {
  .header-logo {
    min-width: var(--space-2xl);
    min-height: var(--space-2xl);
  }

  .side-nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--space-2xl);
  }

  /* A glyph is narrower than a word, so it needs the floor across as well. */
  .side-nav-theme {
    min-width: var(--space-2xl);
    min-height: var(--space-2xl);
  }
}

/* The column does not scroll, so a window too short for it under its lead-in
   would leave the theme control, at its foot, below the window. Such a window
   starts the column just under the bar instead, as a phone on its side does.
   The heights are where the column stops fitting: links at their own height
   for a pointer, floored at 48px for a finger. */
@media (min-width: 1024px) and (max-height: 499px),
       (min-width: 1024px) and (pointer: coarse) and (max-height: 699px) {
  .side-nav {
    top: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-md));
  }
}

/* ===== MAIN CONTENT ===== */
main {
  padding-top: 80px;
}

/* ===== SECTIONS ===== */
.section {
  padding: var(--section-gap) 0;
}

/* An untitled section opens its page, with no title to make room for above it. */
.section--untitled {
  padding-top: 0;
}

.section-title {
  font-family: var(--font-reading);
  font-size: var(--h1-size);
  font-weight: var(--font-medium);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-lg);
  line-height: var(--leading-tight);
  color: var(--color-text);
}

/* ===== HERO ===== */
/* The hero used to claim 40% of the window's height whatever it held, with the
   content centred in it. On a phone that was roughly 90px of nothing above and
   below three lines of text; on a portrait monitor 2560px tall it was near 400px
   each side, and About began halfway down the first screen. How tall the window
   is says nothing about how much the hero holds, so it takes its natural height
   everywhere and only the padding changes with the breakpoint. */
.hero {
  padding: var(--space-lg) 0 var(--space-xl);
  display: flex;
  flex-direction: column;
}

@media (min-width: 768px) {
  .hero {
    padding: var(--space-xl) 0;
  }
}

.hero-name {
  text-wrap: balance;
  font-family: var(--font-reading);
  font-size: clamp(48px, 13vw, 88px);
  font-weight: var(--font-regular);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-none);
  margin-bottom: var(--space-md);
}

.home-page .hero-name {
  color: var(--color-text-body);
}

/* Set the way the prose below it is set: the reading face at the light
   weight, no extra tracking, the same relaxed leading. It used to inherit the
   body's regular weight with wide tracking, which read as a second heading
   under the name rather than as the opening line. This is the shared hero
   subtitle, so the essay subtitle, the essay dateline and the photography
   line all take the same treatment, which is the point: one class, one look.
   The family is named rather than inherited because matching the About
   section is the requirement; --font-ui and --font-reading are the same face
   today and only this line says they have to stay that way here. */
.hero-tagline {
  font-family: var(--font-reading);
  font-size: var(--h4-size);
  font-weight: var(--font-light);
  line-height: var(--leading-relaxed);
  color: var(--color-text-body);
}

/* A label that opens a sentence of the home tagline, such as "Current:". The
   tagline is one paragraph, so the labels are what the eye finds in it. The
   underline is a thin dashed rule, set a step below the letters. */
.hero-label {
  text-decoration: underline dashed;
  text-decoration-thickness: var(--border-thin);
  text-underline-offset: var(--space-xs);
}

.hero-social {
  display: flex;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}

.hero-social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: var(--radius-full);
}

/* The marks rest faint, well below the other glyphs, so that meeting the
   pointer is a real change: they switch, with no fade, to full strength and
   their network's colour. X and GitHub wear black or white, so full strength
   is all they get. */
.hero-social-link svg {
  width: var(--icon-lg);
  height: var(--icon-lg);
  opacity: var(--opacity-faint);
}

/* On white glass the faint rest washes out, so in light mode the marks rest
   in the colour of the about text around them. Hover is left alone. */
html.light .hero-social-link:not(:hover) svg {
  color: var(--color-text-body);
  opacity: 1;
}

.hero-social-link--linkedin:hover {
  color: var(--brand-linkedin);
}

.hero-social-link--gmail:hover .gmail-blue     { stroke: var(--brand-gmail-blue); }
.hero-social-link--gmail:hover .gmail-red      { stroke: var(--brand-gmail-red); }
.hero-social-link--gmail:hover .gmail-dark-red { stroke: var(--brand-gmail-dark-red); }
.hero-social-link--gmail:hover .gmail-yellow   { stroke: var(--brand-gmail-yellow); }
.hero-social-link--gmail:hover .gmail-green    { stroke: var(--brand-gmail-green); }

/* ===== ONE-BIT EARTH (work.html only) ===== */
/* Readout on the left, globe on the right, the controls in their own row beneath the
   globe so nothing ever sits over the sphere or the text. The stage sets no cursor: the
   Mirage cursor on html must stay whole. */
.globe-section {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 380px);
  gap: var(--space-md) var(--space-lg);
  align-items: end;
}

.globe-stage {
  grid-column: 2;
  grid-row: 1;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  touch-action: pan-y;   /* a sideways drag turns the globe; a vertical swipe still scrolls */
  user-select: none;
  -webkit-user-select: none;
}

#globe-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.globe-readout {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* The place reads as the clock's caption: it hugs the time, and the step
   before the sun's lines comes after it. */
.globe-label {
  font-size: var(--text-sm);
  font-weight: var(--font-bold);
  color: var(--color-text-muted);
  line-height: var(--leading-tight);
  margin-bottom: var(--space-xs);
}

.globe-clock {
  font-family: var(--font-reading);
  font-size: var(--text-xl);
  font-weight: var(--font-regular);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-none);
  color: var(--color-text);
  min-width: 12ch;
}

.globe-meta {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
}

.globe-controls {
  grid-column: 2;
  grid-row: 2;
  justify-self: end;
  position: relative;
  z-index: 1;
  display: flex;
  gap: var(--space-md);
}

/* Round controls follow .hero-social-link. */
.globe-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  cursor: pointer;
}

.globe-btn svg {
  width: var(--icon-lg);
  height: var(--icon-lg);
  display: block;
}

#globe-needle {
  transform-origin: 12px 12px;
  transition: transform var(--duration-normal) var(--ease-in-out);
}

.globe-needle-north { fill: var(--color-text); }
.globe-needle-south { fill: var(--color-text-muted); }

@media (max-width: 767px) {
  .globe-section {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-md) var(--space-lg);
  }
  .globe-stage { grid-column: 1 / -1; grid-row: 1; }
  .globe-readout { grid-column: 1; grid-row: 2; }
  .globe-controls { grid-column: 2; grid-row: 2; }

  /* The clock reserves 12 characters so it cannot twitch as the digits change.
     At --text-xl that reservation is wider than the column the readout gets
     beside the controls on a 320px screen, and the row overflowed the page.
     A step down in size keeps the reservation, and with it the steady clock. */
  .globe-clock { font-size: var(--text-lg); }
}

@media (prefers-reduced-motion: reduce) {
  #globe-needle { transition: none; }
}

/* ===== SCROLL CUE =====
   Most of this page is below the first screen and nothing up there said so.
   The cue says it quietly, from inside the same capsule every other control
   in the overlay wears. What draws the eye is the trick the Where Is map uses
   on a country: a brighter outline traces itself around the shape and fades,
   over and over, so movement does the asking instead of contrast. The capsule
   supplies the resting edge the ring used to draw for itself. JS owns when the
   cue shows; this file owns what it looks like and where it rests. */
.scroll-cue {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: var(--chrome-floor);
  z-index: var(--z-header);
  /* Out of the way until JS decides there is something to scroll to. */
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-slower) var(--ease-out),
              scale var(--duration-fast) var(--ease-out);
}

.scroll-cue.is-visible {
  opacity: 1;
  pointer-events: auto;
}

/* The greeting covers the page, and so does the open phone menu; nothing else
   belongs on top of either. */
html[data-intro="playing"] .scroll-cue,
html.has-menu-open .scroll-cue {
  opacity: 0;
  pointer-events: none;
}

/* The ring is the capsule's own outline, traced. It fills the box rather than
   sitting inside it, and overflow stays visible because half of a stroke on
   the edge falls outside. */
.scroll-cue-ring {
  position: absolute;
  top: 0;
  left: 0;
  /* Sized explicitly: an <svg> with neither dimension set falls back to its own
     300x150 default instead of stretching to the box it was insetting into. */
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.scroll-cue-ring rect {
  fill: none;
  stroke-width: var(--border-thin);
}

/* Drawn from nothing to whole, held a moment, then faded out and begun again.
   The dash units are fractions of the perimeter, so the timing holds whatever
   size the ring is. */
.scroll-cue-trace {
  stroke: var(--chrome-glyph);
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  /* Four map-border traces long: one to draw, one to hold, two to rest. */
  animation: scroll-cue-trace calc(4 * var(--duration-cinematic)) var(--ease-glide) infinite;
}

@keyframes scroll-cue-trace {
  0%   { stroke-dashoffset: 1; opacity: 1; }
  25%  { stroke-dashoffset: 0; opacity: 1; }
  40%  { stroke-dashoffset: 0; opacity: 1; }
  55%  { stroke-dashoffset: 0; opacity: 0; }
  100% { stroke-dashoffset: 1; opacity: 0; }
}

.scroll-cue-arrow {
  width: var(--icon-lg);
  height: var(--icon-lg);
  transition: opacity var(--duration-normal) var(--ease-in-out);
}

/* On paper there is nothing to scroll to. */
@media print {
  .scroll-cue { display: none; }
}

/* ===== ABOUT ===== */
.about-content {
  font-family: var(--font-reading);
  font-size: var(--body-size);
  font-weight: var(--font-light);
  overflow-wrap: anywhere;
  line-height: var(--leading-relaxed);
  color: var(--color-text-body);
}

/* The bio wraps around the portrait, newspaper style: on wide screens the photo
   sits top-left and the text widens to the full column once it clears it. On
   narrow screens the photo stacks above the text. flow-root keeps the float
   inside this box, so the career strip below never wraps beside the photo. */
.about-grid {
  --portrait-width: 220px;
  display: flow-root;
}

/* The portrait arrives the way joebell.studio's does. A thumbnail a few pixels
   wide, carried inline in the page as the figure's --portrait-placeholder, fills
   the frame blurred from the first paint, so the reader sees the photo's colours
   at once rather than a hole. The photo itself starts just as blurred, enlarged
   and clear, and main.js marks it loaded, at which point it eases into focus
   over the thumbnail. Paint containment clips both to the frame, overflow
   included, so a box reaching past it is never content sticking out. The frame
   wears the border, so the blur never bleeds over it, and the thumbnail's box
   reaches a blur radius past the frame, which keeps the blur's soft edge outside. */
.about-portrait {
  position: relative;
  contain: paint;
  margin: 0 0 var(--space-lg);
  border-radius: var(--radius-lg);
  border: var(--border-thin) solid var(--color-border-muted);
}

.about-portrait::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--blur-up-blur));
  background: var(--portrait-placeholder) center / cover no-repeat;
  filter: blur(var(--blur-up-blur));
}

.about-portrait img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
  filter: blur(var(--blur-up-blur));
  scale: var(--blur-up-scale);
}

.about-portrait img.is-loaded {
  opacity: 1;
  filter: blur(0);
  scale: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .about-portrait img {
    transition: opacity var(--duration-cinematic) var(--ease-in-out),
                filter var(--duration-cinematic) var(--ease-in-out),
                scale var(--duration-cinematic) var(--ease-in-out);
  }
}

@media (min-width: 768px) {
  /* The top margin is half the leading, so the photo's top edge meets the top
     of the first line's em box rather than the air above it. */
  .about-portrait {
    float: left;
    width: var(--portrait-width);
    margin: calc((var(--leading-relaxed) - 1) * var(--body-size) / 2) var(--space-xl) var(--space-md) 0;
  }
}

.about-content strong {
  font-weight: var(--font-medium);
}

.about-content a,
.timeline-achievement a,
.essay-content a {
  --title-lit: 0;
  color: var(--color-link);
  text-decoration: none;
  background: linear-gradient(currentColor, currentColor) left bottom no-repeat;
  background-size: calc(var(--title-lit) * 100%) var(--border-thin);
  transition: background-size var(--duration-underline-draw) var(--ease-draw),
              color var(--duration-normal) var(--ease-in-out);
}

.about-content a:hover,
.timeline-achievement a:hover,
.essay-content a:hover {
  color: var(--color-link-hover);
}

:is(.about-content, .timeline-achievement, .essay-content) a:focus-visible {
  --title-lit: 1;
}

@media (hover: hover) {
  :is(.about-content, .timeline-achievement, .essay-content) a:hover {
    --title-lit: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  :is(.about-content, .timeline-achievement, .essay-content) a {
    transition: none;
  }
}

@media (forced-colors: active), (hover: none) {
  :is(.about-content, .timeline-achievement, .essay-content) a {
    text-decoration: underline;
    text-decoration-thickness: var(--border-thin);
    text-underline-offset: var(--space-xs);
  }
}

.about-content p {
  margin-bottom: var(--space-md);
}

.about-content p:last-child {
  margin-bottom: 0;
}

/* Prose lists. Nothing else styles a bare <ul>, so without the indent the
   markers hang outside the content box and, on a phone, sit in the page
   gutter rather than beside the text they belong to. */
.about-content ul,
.essay-content ul {
  margin-bottom: var(--space-md);
  padding-left: var(--space-md);
}

.about-content li,
.essay-content li {
  margin-bottom: var(--space-sm);
}

.about-content li:last-child,
.essay-content li:last-child {
  margin-bottom: 0;
}

/* ===== CAREER STRIP ===== */
/* The whole arc on one line: every stop, newest first, closing the bio on
   the home page; the full list is the Work page. render_strip passes the number of stops as
   --stops, because the grid needs one track per stop and only the source
   knows how many there are. The glyph definitions render nothing
   themselves. */
.logo-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Round, and sized like a newspaper byline portrait, because beside a byline
   that is exactly what it stands in for. The Work page wears the same circle so
   the page has one. A wide wordmark still fits: at the tile's waist the circle
   is as wide as the square that a mark of this size would otherwise need. */
.co-mark {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--co-mark-size);
  height: var(--co-mark-size);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.co-mark svg {
  display: block;
  width: 26px;
  height: 26px;
  fill: currentColor;
}

/* A face fills its circle; a glyph sits in the middle of one. The crop is
   pulled above centre so the head lands in the frame rather than the chest. */
.co-mark--photo {
  object-fit: cover;
  object-position: 50% 42%;
}

/* Press-author headshots run monochrome, in keeping with the design system;
   the source image stays colour, so this is a presentation choice, not a baked
   asset. Applied to any author photo that is not the site owner's own byline. */
.co-mark--mono {
  filter: grayscale(1);
}

/* Wide marks keep their aspect and sit inside the tile with air on each side. */
.co-mark--dxc svg {
  width: 34px;
  height: 14px;
}

.co-mark--atxsg svg {
  width: 34px;
  height: 16px;
}

/* MOVE's mark is its whole logo, wider than it is tall, so it takes most of
   the circle's width to stay legible, short of the curve at its corners. */
.co-mark--move svg {
  width: calc(var(--co-mark-size) - 3 * var(--space-xs));
}

/* Rest of World's favicon mark, drawn monochrome so it fills with the shared
   currentColor like every other logo. */
.co-mark--restofworld svg {
  width: 24px;
  height: 24px;
}

/* The strip is laid out around its tile and dot sizes, and its tiles take
   --tile too, so the dots placed from it always clear them. Above the phone
   layout the tiles stand over the rail and the words hang under it, so the
   line parts each company's mark from what the role was, and each dot sits
   centred under its tile. The strip is a flight plan like the Work list, run
   across the page: the padding on the left is room for the plane to depart
   at the tiles' height and bank down onto the rail before the first stop,
   and the rail runs on past the last stop. */
.career-strip {
  --tile: var(--co-mark-size);
  --dot: 10px;
  --rail-centre: calc(var(--tile) + var(--space-sm) + var(--dot) / 2);
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--stops, 6), minmax(0, 1fr));
  column-gap: var(--space-sm);
  padding-left: calc(2 * var(--space-xl));
}

/* A column, so a label that wraps to two lines pushes only its own dates down
   and every node's dates still meet on one baseline at the bottom. */
.career-node {
  position: relative;
  display: flex;
  flex-direction: column;
}

.career-node::before {
  content: '';
  position: absolute;
  left: calc((var(--tile) - var(--dot)) / 2);
  top: calc(var(--rail-centre) - var(--dot) / 2);
  /* The page reset skips pseudo-elements, so without this the border adds to
     the box and the dot's centre drifts 2px off the rail. */
  box-sizing: border-box;
  width: var(--dot);
  height: var(--dot);
  border-radius: var(--radius-full);
  border: var(--border-medium) solid var(--color-text-muted);
  background: var(--color-bg);
  z-index: 1;
}

/* The rail runs through the centre of every dot; qa/responsive-check.mjs
   measures it. Nothing sits on a dot that could hide the line meeting it.
   Filled: the current role from the start, and every stop the plane has reached. */
.career-node.is-current::before,
.career-node.is-flown::before {
  border-color: var(--color-text);
  background: var(--color-text);
}

/* Tile and name hang from the same top line, so a label that wraps grows
   downward and the tiles stay level across the strip. */
.career-co {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

.career-co .co-mark {
  width: var(--tile);
  height: var(--tile);
}

/* Six stops share a column capped at 680px less the take-off room, which
   leaves 96px a stop. A tile beside the name leaves 62px for the text, and
   GrabMaps alone needs 70px and cannot be broken. Above the phone layout the
   tile therefore sits on its own line and the name takes the column's full
   width. Each further stop narrows the column: seven still clears 70px, eight
   lands on it exactly and GrabMaps spills at any narrower column.
   A ninth role is a reason to rethink the strip, not to widen this rule.
   The gap between tile and name holds the rail: a gap, the dot, a gap. */
@media (min-width: 768px) {
  .career-co {
    flex-direction: column;
    gap: calc(2 * var(--space-sm) + var(--dot));
  }
}

.career-name {
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  line-height: var(--leading-normal);
  color: var(--color-text);
}

/* A label that wraps splits evenly, rather than leaving its last word alone
   on a line. */
.career-name small {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--font-regular);
  line-height: var(--leading-snug);
  color: var(--color-text-muted);
  text-wrap: balance;
}

.career-dates,
.timeline-date {
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-wider);
  color: var(--color-text-muted);
}

/* On the bottom of every node whatever the label's line count. */
.career-dates {
  margin-top: auto;
  padding-top: var(--space-sm);
}

/* Under the bio and portrait, with the same air the toggle row once left.
   The glyph definitions sit between the two, so a general sibling. */
.about-grid ~ .career-strip {
  margin-top: var(--space-xl);
}

/* ===== WORK TIMELINE =====
   The list is a flight plan. One dashed route runs down the left of the stops,
   newest first, and the site's plane flies it as far as the reader has
   scrolled: the route behind the plane is drawn solid, and every stop it has
   reached is filled in. flight-plan.js draws the route from where the stops
   are, departing from over the first mark and banking onto the rail, and runs
   it on past the last stop; the rules here are the route's and the plane's
   looks. Without the script nothing is drawn and the plane stays hidden. */
.timeline {
  position: relative;
  /* Room for the plane to depart and bank onto the rail above the first stop. */
  padding-top: calc(2 * var(--space-2xl));
}

.timeline-item {
  position: relative;
  padding-left: var(--space-lg);
  padding-bottom: var(--space-lg);
}

.timeline-item:last-child {
  padding-bottom: 0;
}

/* The picture the route is drawn in covers the plan, and the script trims it
   to the route's end so the fade lands there: the tail dissolves rather than
   stopping. Across the page the fade runs to the right instead. */
.flight-path {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--space-2xl)), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--space-2xl)), transparent);
}

.flight-plan.is-across .flight-path {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--space-2xl)), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - var(--space-2xl)), transparent);
}

/* The route. Dashes, not a line: the plane has not been this way yet. */
.flight-route {
  fill: none;
  stroke: var(--color-border);
  stroke-width: var(--border-medium);
  stroke-dasharray: 3 4;
}

/* The part flown, solid, from the gate to the plane: the same path, dashed
   once by the script to the plane's distance along it. */
.flight-flown {
  fill: none;
  stroke: var(--color-text);
  stroke-width: var(--border-medium);
  stroke-dasharray: 0 1;
}

/* The plane, at the head of the flown route and nose along it, which the
   script sets. Painted in the text colour edged with the page colour, so it
   stays whole where it crosses a dot or the line. It is the route's sibling,
   placed from the plan, so its width is never the route's overflow. */
/* The plane is decoration, like its route, so the pointer passes through it: to
   the stop or card beneath, and without counting as a stop of its own. */
.flight-plane {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  width: 14px;
  height: 22px;
  overflow: visible;
  translate: -50% -50%;
  rotate: 180deg;
  fill: var(--color-text);
  stroke: var(--color-bg);
  stroke-width: 6;
  stroke-linejoin: round;
  paint-order: stroke;
}

.flight-plan:not(.is-drawn) .flight-plane {
  visibility: hidden;
}

/* A stop's dot sits on the route, level with the middle of the mark. */
.timeline-item::before {
  content: '';
  position: absolute;
  left: -5px;
  top: calc((var(--co-mark-size) - 10px) / 2);
  box-sizing: border-box;
  width: 10px;
  height: 10px;
  border: var(--border-medium) solid var(--color-border);
  background: var(--color-bg);
  border-radius: var(--radius-full);
  transition: background-color var(--duration-slow) var(--ease-in-out),
              border-color var(--duration-slow) var(--ease-in-out);
}

/* Filled: the current role from the start, and every stop the plane has reached. */
.timeline-item.is-current::before,
.timeline-item.is-flown::before {
  border-color: var(--color-text);
  background: var(--color-text);
}

/* A Work mark is the link to the company and a group of one. Its wrapper is
   the footing the pointer is judged against, the size of the mark exactly,
   so the mark can rise off it without a pointer on its lower edge setting it
   bouncing, which a :hover on the mark itself does. */
#work-rows .bubbles {
  display: flex;
}

/* A mark gives a small bounce as the plane lands beside it. */
.flight-plan .is-flown .co-mark {
  animation: flight-landing var(--duration-slower) var(--ease-spring);
}

@keyframes flight-landing {
  40% { scale: 1.1; }
}

@media (prefers-reduced-motion: reduce) {
  .flight-plan .is-flown .co-mark {
    animation: none;
  }
}

.timeline-title {
  font-family: var(--font-reading);
  font-size: var(--h3-size);
  font-weight: var(--font-bold);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-xs);
  line-height: var(--leading-snug);
}

/* In the work list the title is company and role on one line with the
   company's mark, a size down from a standalone title, and the paragraph a
   step under that so the title leads. */
#work-rows .timeline-title {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  line-height: var(--leading-normal);
}

.timeline-role {
  font-weight: var(--font-regular);
}

/* Tenure sits between the title and the story. */
.timeline-date {
  margin-bottom: var(--space-sm);
}

.timeline-role::before {
  content: '· ';
  color: var(--color-text-muted);
}

#work-rows .timeline-achievement {
  font-size: var(--text-sm);
}

.timeline-achievement {
  font-family: var(--font-reading);
  font-size: var(--body-size);
  font-weight: var(--font-light);
  line-height: var(--leading-relaxed);
  color: var(--color-text-body);
}

/* Medium turned every number in the paragraph into a shout, and a paragraph
   where twenty spans shout has no emphasis left. Holding the paragraph's own
   light weight went too far the other way: colour was then the only cue, and
   #F5F5F5 against #FFFFFF is 1.09:1, which is no cue at all. Literata is
   served at 300, 400, 500 and 700, so regular is the smallest step there is,
   and it is the one to take. Colour comes with it because two faint signals
   agreeing read as deliberate where one does not. */
.timeline-achievement strong {
  font-weight: var(--font-regular);
  color: var(--color-text);
}

/* Videos: 16:9 players at one size, two to a row on desktop, one on a phone.
   auto-fill keeps the empty second track, so a lone player stays that size
   rather than stretching across. The same block serves a role's demos and a
   press item's talk. The minimum yields to the column on the narrowest phones,
   where a role's rail and indent leave less than the track would claim. */
.timeline-videos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.video-embed {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-bg-elevated);
  cursor: pointer;
}

/* The poster in the site's monochrome, like the portrait; colour arrives only
   with the player. */
.video-embed img,
.video-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.video-embed img {
  object-fit: cover;
  filter: grayscale(1);
  transition: filter var(--duration-slow) var(--ease-in-out);
}

.video-embed:hover img,
.video-embed:focus-visible img {
  filter: grayscale(0.4);
}

.video-embed:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.video-play {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: var(--color-bg);
  color: var(--color-text);
  opacity: var(--opacity-subtle);
}

.video-play svg {
  width: var(--icon-md);
  height: var(--icon-md);
  margin-left: 2px;
}

/* On a phone the stops stand in a column on one rail, dates to the right,
   and the plane flies down it instead of across: it departs above the first
   tile, as on the Work page, and the route runs on below the last. */
@media (max-width: 767px) {
  .career-strip {
    grid-template-columns: 1fr;
    row-gap: var(--space-md);
    padding-top: calc(2 * var(--space-2xl));
    padding-bottom: var(--space-2xl);
    padding-left: var(--space-lg);
  }

  .career-node {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
  }

  .career-node::before {
    left: calc(-1 * var(--space-lg));
    top: 50%;
    margin-top: calc(var(--dot) / -2);
  }

  .career-dates {
    margin-top: 0;
    padding-top: 0;
    white-space: nowrap;
  }

  /* Company on one line, role on the next: the separator only makes sense
     when the two share a line. */
  .timeline-role {
    flex-basis: 100%;
  }

  .timeline-role::before {
    content: none;
  }
}

/* ===== VISUALLY HIDDEN ===== */
/* Text a screen reader hears that the page does not show, because something
   on the page already shows it another way. */
.visually-hidden {
  position: absolute;
  width: var(--border-thin);
  height: var(--border-thin);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ===== ARTICLES ===== */
/* Press as a list of rows, each read left to right: who wrote the piece and
   where it ran, as bubbles, then the headline, then the date at the far edge.
   Space alone parts one row from the next. The bubbles come after the headline
   in the markup, so they are placed into the first column by hand. The column
   holds two overlapping circles, so a row with one keeps its headline in line. */
.article-item {
  --bubble-column: calc(2 * var(--co-mark-size) - var(--space-sm));
  display: grid;
  grid-template-columns: var(--bubble-column) minmax(0, 1fr) auto;
  column-gap: var(--space-md);
  align-items: baseline;
  padding: var(--space-lg) 0;
}

.section-title + .article-item {
  padding-top: 0;
}

/* A bubble rises to meet the pointer with the rest of its group. main.js moves
   --shift, --scale-active, --ease-bubble and the stacking as the pointer moves;
   at rest they hold nothing and the bubble sits flat. The transition names
   scale as well, for the social links, which give way through scale when
   pressed. */
.bubble {
  transform: translateY(var(--shift, 0px)) scale(var(--scale-active, 1));
  transition: transform var(--duration-avatar) var(--ease-bubble, var(--ease-avatar-in)),
              scale var(--duration-fast) var(--ease-out);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .bubble {
    transition: none;
    transform: none;
    will-change: auto;
  }
}

/* The bubbles are the site's byline circles, overlapping the way a list of
   contributors is drawn: the author first, the publication over its edge. A
   ring of the page's own colour cuts the two apart where they meet. */
.press-bubbles {
  grid-column: 1;
  grid-row: 1;
  align-self: center;
  display: flex;
}

.press-bubbles > .co-mark {
  z-index: 1;
  box-shadow: 0 0 0 var(--border-medium) var(--color-bg),
              var(--chrome-shadow),
              inset 0 0 0 var(--border-thin) var(--chrome-edge);
}

.press-bubbles > .co-mark + .co-mark {
  margin-left: calc(-1 * var(--space-sm));
}

/* With no photo or glyph on file, a bubble wears initials. */
.co-mark--initials {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  letter-spacing: var(--tracking-wide);
  text-decoration: none;
}

.article-lines {
  grid-column: 2;
  min-width: 0;
}

/* The headline is the row's link. It carries no underline at rest; the
   pointer draws one in, as it does under a project's or an essay's title
   (TITLE LINKS). */
.article-title {
  font-family: var(--font-reading);
  font-size: var(--body-size);
  font-weight: var(--font-regular);
  line-height: var(--leading-snug);
  color: var(--color-text);
}

.article-title a {
  color: inherit;
  text-decoration: none;
}

.article-excerpt {
  margin-top: var(--space-sm);
  font-family: var(--font-reading);
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  line-height: var(--leading-relaxed);
}

/* --color-text-subtle, the excerpt's grey, so the date reads at the strength
   of the words beside it. */
.article-date {
  grid-column: 3;
  grid-row: 1;
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  white-space: nowrap;
}

.article-item > .timeline-videos {
  grid-column: 1 / -1;
  margin-top: var(--space-md);
}

/* A phone has no room for a third column, so the date joins the text and the
   grid drops the column, gap and all. */
@media (max-width: 767px) {
  .article-item {
    grid-template-columns: var(--bubble-column) minmax(0, 1fr);
  }

  .article-date {
    grid-column: 2;
    grid-row: auto;
    margin-top: var(--space-sm);
    font-size: var(--text-xs);
  }
}

/* ===== FEATURED STRIP =====
   The talks, as project cards are set on gabrielvaldivia.com: a strip the
   reader scrolls across, each card a preview of its talk playing under a shade
   with the title on it. The strip steps out of the reading column to both
   edges of the window (the script measures the window, so a scrollbar is not
   counted; the audit knows a child laid out edge to edge as a bleed). Its
   gutters keep the cards inside the page's own margins, and on a wide screen
   clear of the nav on the left. A card is nine tenths of the room between
   the gutters, so the next one shows at the edge; its frame is 3:2, and the
   16:9 preview is cropped to it from the centre.

   On a phone it is a slideshow instead: one card the height of the screen,
   the width of the window, snapping one at a time, with a bar per card at
   the top that fills as the card's time runs, in the manner of a story. It
   arrives scaled down inside the window's gutters with rounded corners and
   grows to the window's full width as it is scrolled to the top; the script
   sets the two custom properties from the scroll. */
.featured-strip {
  --featured-scale: 1;
  --featured-radius: var(--radius-xl);
  --featured-gutter: var(--space-md);
  --featured-width: var(--featured-window, 100%);
  position: relative;
  width: var(--featured-width);
  margin-inline: calc(50% - var(--featured-width) / 2);
}

@media (min-width: 768px) {
  .featured-strip {
    --featured-gutter: var(--space-lg);
  }
}

@media (min-width: 1024px) {
  .featured-strip {
    --featured-gutter: max(var(--nav-column), (var(--featured-width) - var(--container-xl)) / 2);
  }
}

.featured-strip-viewport {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--featured-gutter);
  scrollbar-width: none;
}

.featured-strip-viewport::-webkit-scrollbar {
  display: none;
}

.featured-strip-viewport:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

@media (pointer: fine) {
  .featured-strip-viewport {
    cursor: grab;
  }

  .featured-strip-viewport.is-dragging {
    cursor: grabbing;
    user-select: none;
  }
}

.featured-strip-track {
  display: flex;
  flex: none;
  width: max-content;
  gap: var(--space-lg);
  padding-inline: var(--featured-gutter);
}

.featured-card {
  position: relative;
  flex: none;
  width: calc((var(--featured-width) - 2 * var(--featured-gutter)) * 0.9);
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--color-bg-elevated);
  scroll-snap-align: center;
}

.featured-card:first-child {
  scroll-snap-align: start;
}

.featured-card:last-child {
  scroll-snap-align: end;
}

.featured-card-media,
.featured-card-media video,
.featured-card-media iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.featured-card-media video {
  object-fit: cover;
}

/* The poster is the card's background, one per cut, set on the card by the
   build: a background loads only for the window it applies to, which a video's
   poster attribute cannot do. The video is clear until its first frame, and its
   first frame is the poster. */
.featured-card-media {
  background: var(--poster) center / cover no-repeat;
}

.featured-card-shade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 55%, var(--media-shade));
}

/* The whole frame is the link to the talk; the caption lets the pointer
   through to it and the capsules sit above it. */
.featured-card-link {
  position: absolute;
  inset: 0;
}

.featured-card-link:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: calc(-1 * var(--focus-width));
}

.featured-card-caption {
  position: absolute;
  left: var(--space-lg);
  right: calc(2 * var(--chrome-size) + var(--space-sm) + 2 * var(--space-lg));
  bottom: var(--space-lg);
  pointer-events: none;
  color: var(--media-text);
}

.featured-card-title {
  font-family: var(--font-reading);
  font-size: var(--h2-size);
  font-weight: var(--font-medium);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  text-wrap: balance;
}

.featured-card-excerpt {
  margin-top: var(--space-sm);
  font-size: var(--text-sm);
  font-weight: var(--font-light);
  line-height: var(--leading-snug);
  color: var(--media-text-muted);
  text-wrap: pretty;
}

.featured-card-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  list-style: none;
}

.featured-card-pills li {
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-full);
  box-shadow: inset 0 0 0 var(--border-thin) var(--media-edge);
  font-size: var(--text-xs);
  font-weight: var(--font-regular);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  line-height: var(--leading-loose);
  white-space: nowrap;
}

.featured-card-controls {
  position: absolute;
  right: var(--space-lg);
  bottom: var(--space-lg);
  display: flex;
  gap: var(--space-sm);
}

.featured-card-controls button,
.featured-card-controls a {
  padding: 0;
  width: var(--chrome-size);
}

.featured-card-controls svg {
  width: var(--icon-lg);
  height: var(--icon-lg);
  opacity: var(--opacity-subtle);
  transition: opacity var(--duration-normal) var(--ease-in-out);
}

.featured-card-controls button:hover svg,
.featured-card-controls a:hover svg {
  opacity: 1;
}

.featured-card-controls button:focus-visible,
.featured-card-controls a:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* One sound glyph shows at a time, by the capsule's pressed state. */
.featured-card-sound[aria-pressed="true"] .icon-off,
.featured-card-sound[aria-pressed="false"] .icon-on {
  display: none;
}

/* The play glyph sits a touch right of centre, as on the press posters. */
.featured-card-watch svg {
  margin-left: 2px;
}

/* The bars are the phone's; a wider screen shows the next card at the edge,
   which says the same thing. */
.featured-strip-bars {
  display: none;
}

@media (max-width: 767px) {
  .featured-strip {
    --featured-gutter: 0px;
  }

  .featured-strip-viewport {
    border-radius: var(--featured-radius);
    transform: scale(var(--featured-scale));
    transform-origin: center;
  }

  .featured-strip-track {
    gap: 0;
  }

  .featured-card {
    width: var(--featured-width);
    aspect-ratio: auto;
    height: 100vh;
    height: 100svh;
    border-radius: 0;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }

  .featured-card-media {
    background-image: var(--poster-phone);
  }

  .featured-card-shade {
    background: linear-gradient(to bottom, transparent 50%, var(--media-shade));
  }

  /* The caption and the card's own capsules rest on the floor every control
     at the foot of the window shares. */
  .featured-card-caption {
    left: var(--space-md);
    right: calc(var(--chrome-size) + 2 * var(--space-md));
    bottom: var(--chrome-floor);
  }

  .featured-card-controls {
    right: var(--space-md);
    bottom: var(--chrome-floor);
    flex-direction: column-reverse;
  }

  /* Each bar sits in a 44px square so a thumb can land on it; the line
     itself is two pixels, the fill grows from the left as the time runs. */
  .featured-strip-bars {
    position: absolute;
    top: var(--space-md);
    left: var(--space-md);
    z-index: 1;
    display: flex;
  }

  .featured-strip-bar {
    display: flex;
    align-items: center;
    width: var(--space-2xl);
    height: var(--space-2xl);
    padding: 0 var(--space-xs);
    border: 0;
    background: none;
    cursor: pointer;
  }

  .featured-strip-bar:focus-visible {
    outline: var(--focus-width) solid var(--focus-color);
    outline-offset: var(--focus-offset);
    border-radius: var(--radius-sm);
  }

  .featured-strip-bar span {
    position: relative;
    display: block;
    width: 100%;
    height: var(--border-medium);
    border-radius: var(--radius-full);
    background: var(--media-edge);
    overflow: hidden;
  }

  .featured-strip-bar span::after {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--media-text);
    transform: scaleX(var(--featured-progress, 0));
    transform-origin: left;
  }

  .featured-strip-bar[aria-current="true"] span::after {
    --featured-progress: var(--featured-fill, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .featured-strip-viewport {
    transform: none;
    border-radius: 0;
  }
}

/* Closed, the last row shown dims and fades out toward Show more, the way a
   list hints that it goes on. The mask's colours set only how much of the row
   shows through, from muted at the headline to nothing at its foot. Opening
   the list lifts it. */
.article-item:has(+ .press-rest:not([open])) {
  mask-image: linear-gradient(to bottom, rgb(0 0 0 / var(--opacity-muted)), transparent);
}

/* Show more, under the first items: the summary of the <details> that holds
   the rest, a hairline capsule centred under the list and drawn up over the
   foot of the faded row. Positioned so it paints above that row, and filled
   with the page so the row stops at its edge. The summary is a block sized to
   its label, which does the flex layout, since browsers have not always
   honoured flex on <summary>. The edge is --color-border rather than the glass
   edge, which is a near-white 4% in the light theme and would leave a label
   floating with no shape around it. */
.press-more {
  position: relative;
  display: block;
  width: fit-content;
  margin: calc(-1 * var(--space-lg)) auto 0;
  list-style: none;
  padding: 0 var(--space-lg);
  border-radius: var(--radius-full);
  background: var(--color-bg);
  box-shadow: inset 0 0 0 var(--border-thin) var(--color-border);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  font-weight: var(--font-regular);
  color: var(--color-text);
  transition: box-shadow var(--duration-normal) var(--ease-in-out);
}

/* Safari draws its disclosure triangle from this, not from list-style. */
.press-more::-webkit-details-marker {
  display: none;
}

.press-more-label {
  display: inline-flex;
  align-items: center;
  min-height: var(--space-2xl);
}

/* Open, the list holds everything Show more offered, so Show more goes. */
.press-rest[open] > .press-more {
  display: none;
}

.press-more:hover {
  box-shadow: inset 0 0 0 var(--border-thin) var(--color-text-muted);
}

.press-more:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* ===== TITLE LINKS ===== */
/* A title in Press, Projects and Writing answers the pointer the way
   marcbaselga.com's project names do: an underline draws in under the words,
   left to right. Only the title answers, not the
   line under it. The words sit in a span inside the link, so the underline
   follows them however the link is laid out for a finger: a headline that
   wraps is underlined line after line, in reading order.

   --title-lit is the one switch, 0 at rest and 1 lit, and each amount below
   is a token times it. A pointer that hovers lights a title by resting on it
   and the keyboard by landing on it. A screen that cannot hover has nothing
   resting on the page, so main.js lights the title at the middle of the
   window instead (is-lit). Such a screen is kept out of :hover, which it would
   leave stuck on the last title tapped.

   The words stay where they are, though marcbaselga.com's lean in and rise a
   pixel. A link set in a line of text answers the pointer only over its
   words, so a pointer resting on a title's first letter or its foot found
   itself off the words once they moved, and hover flickered on and off for as
   long as it stayed. */
/* A word too long for its line breaks rather than push the row past the
   page's edge, beside Writing's date on a narrow phone above all. Nothing else
   wraps differently: a title still breaks at its spaces first.
   word-break: break-word says the same to Safari before 15.4. */
.title-text {
  --title-lit: 0;
  background: linear-gradient(currentColor, currentColor) left bottom no-repeat;
  background-size: calc(var(--title-lit) * 100%) var(--border-thin);
  transition: background-size var(--duration-underline-draw) var(--ease-draw);
  word-break: break-word;
  overflow-wrap: anywhere;
}

a.is-lit .title-text {
  --title-lit: 1;
}

/* A rule of its own: a browser that does not know :focus-visible drops any
   rule naming it, and would take the touch light down with it. */
a:focus-visible .title-text {
  --title-lit: 1;
}

@media (hover: hover) {
  a:hover .title-text {
    --title-lit: 1;
  }
}

/* Projects and Writing end each title with an arrow that says it goes
   somewhere; Press's headlines go without. build.py leaves .title-end empty
   after the words, and it starts with a word joiner, which a line may not
   break before, and does not wrap, so the arrow stays with the last word. The
   joiner is generated here rather than written in the page, where it would
   ride along in the link's text to analytics; its alternative text is empty,
   so a screen reader passes over it. Only when the last word with its arrow
   is wider than the whole line may they part, rather than overflow it. The
   underline runs under the arrow too, as it does on marcbaselga.com. Lit, the
   arrow brightens from its resting opacity and nudges up and out, turning a
   little. It is drawn rather than set as the ↗ character, which a device
   would fill from whichever of its fonts has the glyph. */
.title-end {
  white-space: nowrap;
}

.title-end::before {
  content: '\2060';
  content: '\2060' / '';
}

.title-end::after,
:is(.about-content, .timeline-achievement, .essay-content) a::after {
  --arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' \
fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E\
%3Cpath d='M4 20 20 4M11 4h9v9'/%3E%3C/svg%3E");
  content: '';
  display: inline-block;
  width: var(--title-link-arrow-size);
  height: var(--title-link-arrow-size);
  margin-left: var(--title-link-arrow-gap);
  background-color: currentColor;
  -webkit-mask: var(--arrow) center / contain no-repeat;
  mask: var(--arrow) center / contain no-repeat;
  opacity: max(var(--title-link-arrow-rest), var(--title-lit));
  translate: calc(var(--title-lit) * var(--title-link-arrow-shift))
             calc(var(--title-lit) * var(--title-link-arrow-shift) * -1);
  rotate: calc(var(--title-lit) * var(--title-link-arrow-tilt));
  transition: opacity var(--duration-slow) var(--ease-nudge),
              translate var(--duration-slow) var(--ease-nudge),
              rotate var(--duration-slow) var(--ease-nudge);
}

/* Reduced motion keeps what says a title is lit, the underline and the
   arrow's full strength, and drops every movement: no draw and no nudge. On a
   phone the light passes from row to row as the reader scrolls, and those
   small jumps would repeat all the way down the list. */
@media (prefers-reduced-motion: reduce) {
  .title-text {
    transition: none;
  }

  .title-end::after,
  :is(.about-content, .timeline-achievement, .essay-content) a::after {
    translate: none;
    rotate: none;
    transition: none;
  }
}

/* Forced colours drop a gradient and repaint a background as the page, which
   would take the drawn underline and the masked arrow with them. There, a lit
   title is underlined as text is, and the arrow is painted in the system's
   colour for a link, visited or not, as its words are. currentColor would not
   do: an element let out of forced colours reads the page's own text colour.
   The arrow shows at full strength, since half would undo the contrast the
   reader asked for. */
@media (forced-colors: active) {
  a.is-lit .title-text {
    text-decoration: underline;
  }

  a:focus-visible .title-text {
    text-decoration: underline;
  }

  .title-end::after,
  :is(.about-content, .timeline-achievement, .essay-content) a::after {
    forced-color-adjust: none;
    background-color: LinkText;
    opacity: 1;
  }

  a:visited .title-end::after,
  :is(.about-content, .timeline-achievement, .essay-content) a:visited::after {
    background-color: VisitedText;
  }
}

@media (forced-colors: active) and (hover: hover) {
  a:hover .title-text {
    text-decoration: underline;
  }
}

/* A title on a touch screen is a control standing on its own, not a phrase in
   a sentence, so it takes a row tall enough for a finger to land on, with its
   words centred in it. A flex row centres them in every browser, but turns
   each child into a box, and an underline drawn on a box runs once along its
   foot. So the row centres .title-box, which build.py puts round the words,
   and inside it .title-text stays a line of text, underlined line by line. */
@media (pointer: coarse) {
  .article-title a,
  .project-title a,
  .essay-title {
    display: inline-flex;
    align-items: center;
    min-height: var(--space-2xl);
  }
}

/* ===== PROJECTS LIST ===== */
/* A project is its name over a line on what it is. The name is set as an
   essay's title is and the line as an essay's meta, so Projects reads as one
   family with Writing below it. */
.projects-list {
  display: flex;
  flex-direction: column;
}

.project-item {
  padding: var(--space-lg) 0;
}

.project-item:first-child {
  padding-top: 0;
}

.project-title {
  font-family: var(--font-reading);
  font-size: var(--text-lg);
  font-weight: var(--font-light);
  color: var(--color-text);
}

.project-title a {
  color: inherit;
  text-decoration: none;
}

.project-line {
  margin-top: var(--space-xs);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  color: var(--color-text-muted);
}

/* ===== ESSAYS LIST ===== */
.essays-list {
  display: flex;
  flex-direction: column;
}

.essay-item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: var(--space-lg) 0;
  gap: var(--space-md);
}

.essay-item:first-child {
  padding-top: 0;
}

.essay-title {
  font-family: var(--font-reading);
  font-size: var(--text-lg);
  font-weight: var(--font-light);
  color: var(--color-text);
  text-decoration: none;
}

/* On touch widths the title is a row tall enough to land on (TITLE LINKS),
   and the date centres against it. */
@media (pointer: coarse) {
  .essay-item {
    align-items: center;
  }
}

.essay-meta {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* ===== ESSAY PAGE ===== */
/* An essay's headline and the line under it, set the way the New York Times
   sets an article's: the headline bold at a reading size, well under the
   display size the other pages open with, 2px above the Times' own and on its
   leading, and the subheading in the regular weight, a step above the prose,
   in the body's colour. Both sizes step up at the tablet width with the type
   scale on :root. */
.essay-headline {
  font-family: var(--font-reading);
  font-size: var(--headline-size);
  font-weight: var(--font-bold);
  line-height: var(--leading-headline);
  margin-bottom: var(--space-sm);
}

.essay-summary {
  font-family: var(--font-reading);
  font-size: var(--summary-size);
  font-weight: var(--font-regular);
  line-height: var(--leading-tight);
  color: var(--color-text-body);
}

/* The Listen button, under the subheading where the Times places its own: a
   round play control, then "Listen · 5:14 min". Once the audio starts the
   label gives way to the Times' player: the time heard, a bar to seek with,
   and the time left at its end. The control is the site's 44px circle, which
   is also as small as a finger's target should be. */
.essay-listen {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  color: var(--color-text);
}

.essay-listen-play {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--co-mark-size);
  height: var(--co-mark-size);
  border: var(--border-thin) solid var(--color-border);
  border-radius: var(--radius-full);
  background: none;
  color: inherit;
  cursor: pointer;
  transition: scale var(--duration-fast) var(--ease-out);
}

.essay-listen-play svg {
  width: var(--listen-icon-size);
  height: var(--listen-icon-size);
  fill: currentColor;
}

.essay-listen-pause-icon,
.essay-listen.is-playing .essay-listen-play-icon {
  display: none;
}

.essay-listen.is-playing .essay-listen-pause-icon {
  display: inline;
}

.essay-listen-progress {
  flex: 1;
  min-width: 0;
  max-width: 480px;
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.essay-listen-progress[hidden] {
  display: none;
}

/* Figures of one width, so the times do not shuffle as the seconds turn. */
.essay-listen-time,
.essay-listen-elapsed,
.essay-listen-remaining {
  font-variant-numeric: tabular-nums;
}

/* The Times' bar: a thin track and a small dot, in the proportion they keep
   to its play button. The part heard is drawn in the text's colour, up to the
   --played share listen.js sets, and the rest in the Times' mid grey, which
   is the text's colour at 35%: about #A6A6A6 on white, and a grey that still
   shows on black, where no token sits. The input stands a finger's 44px tall,
   though the bar it draws is the track's height. */
.essay-listen-seek {
  --seek-unheard: color-mix(in srgb, var(--color-text) 35%, transparent);
  appearance: none;
  -webkit-appearance: none;
  flex: 1;
  min-width: 0;
  height: var(--co-mark-size);
  margin: 0;
  background: none;
  cursor: pointer;
}

.essay-listen-seek::-webkit-slider-runnable-track {
  height: var(--listen-track-height);
  border-radius: var(--radius-full);
  background: linear-gradient(to right, var(--color-text) var(--played, 0%),
                              var(--seek-unheard) var(--played, 0%));
}

.essay-listen-seek::-moz-range-track {
  height: var(--listen-track-height);
  border-radius: var(--radius-full);
  background: linear-gradient(to right, var(--color-text) var(--played, 0%),
                              var(--seek-unheard) var(--played, 0%));
}

.essay-listen-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--listen-dot-size);
  height: var(--listen-dot-size);
  /* centred on the track; Firefox centres it on its own */
  margin-top: calc((var(--listen-track-height) - var(--listen-dot-size)) / 2);
  border: none;
  border-radius: var(--radius-full);
  background: var(--color-text);
}

.essay-listen-seek::-moz-range-thumb {
  width: var(--listen-dot-size);
  height: var(--listen-dot-size);
  border: none;
  border-radius: var(--radius-full);
  background: var(--color-text);
}

/* The byline as the Times lays it out: the portrait in the site's circle, then
   "By" and the name. The Times sets the name in bold and underlines it; here it
   keeps the regular weight and no underline, so the headline is the one bold
   line in the header. The whole row links to the About section, which makes it
   the portrait's 44px tall, a target a finger can find. The date sits under
   it, flush with the portrait, a step smaller. */
.essay-byline {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  font-weight: var(--font-regular);
  color: var(--color-text);
}

.essay-byline a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-md);
  color: inherit;
  text-decoration: none;
}

.essay-date {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--color-text-body);
}

/* The header is set the way the Times sets one: a hairline under the summary,
   then the Listen button, the byline and the date, each a step of the spacing
   scale from the last, and the piece the same step below. The rule goes on
   whatever follows the summary, Listen when there is audio and the byline
   when there is not. The hero's own bottom padding gives way to the piece's. */
.essay-headline + .essay-listen,
.essay-summary + .essay-listen,
.essay-headline + .essay-byline,
.essay-summary + .essay-byline {
  border-top: var(--border-thin) solid var(--color-border-muted);
  padding-top: var(--space-lg);
  margin-top: var(--space-lg);
}

.hero:has(.essay-headline) {
  padding-bottom: 0;
}

.hero:has(.essay-headline) + .essay-section {
  padding-top: var(--space-lg);
}

.essay-section {
  padding: var(--space-xl) 0;
}

.essay-section-title {
  font-family: var(--font-reading);
  font-size: var(--h2-size);
  font-weight: var(--font-bold);
  margin-bottom: var(--space-lg);
  color: var(--color-text);
}

/* Photo captions are prose too, and read at the essay's size and measure,
   so they share its rule rather than keeping a copy that could drift. */
.essay-content,
.gallery-caption,
.gallery-viewer-caption {
  font-family: var(--font-reading);
  font-size: var(--body-size);
  font-weight: var(--font-light);
  overflow-wrap: anywhere;
  line-height: var(--leading-relaxed);
  color: var(--color-text-body);
}

.essay-content strong {
  font-weight: var(--font-medium);
}

.essay-content p {
  margin-bottom: var(--space-md);
}

.essay-content ol {
  margin-left: var(--space-lg);
  margin-bottom: var(--space-md);
  color: var(--color-text-body);
}

/* A subsection inside a section, the way a paper breaks a long piece. */
.essay-content h3 {
  font-family: var(--font-reading);
  font-size: var(--h4-size);
  font-weight: var(--font-bold);
  margin: var(--space-lg) 0 var(--space-sm);
  color: var(--color-text);
}

/* The lead photo runs the full width of the text, the way the Times runs one,
   on the same left edge as everything above it. It is cropped to 3:2, so a
   tall photo does not fill the screen before the piece begins; the crop is
   centred, and the file keeps the whole picture for the preview card. */
.essay-image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-md);
  margin: 0 0 var(--space-lg);
}

/* ===== FOOTER ===== */
.footer {
  padding: var(--space-xl) 0;
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.footer-name {
  color: var(--color-text);
}

.footer-updated {
  white-space: nowrap;
}

/* Too narrow for one line, as main.js measures it: the time takes a line of its
   own and the dot goes, rather than hanging at the end of the first. */
.footer p.is-wrapped .footer-sep { display: none; }
.footer p.is-wrapped .footer-updated { display: block; }

/* ===== ANIMATIONS ===== */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-in {
  animation: fadeInUp var(--duration-slower) var(--ease-out) forwards;
  opacity: 0;
}

.delay-1 { animation-delay: 100ms; }
.delay-2 { animation-delay: 200ms; }
.delay-3 { animation-delay: 300ms; }

.work-intro {
  margin-bottom: var(--space-lg);
}

/* ===== INTRO GREETING SEQUENCE (index.html only) ===== */
/* Hidden unless the <head> script decides the intro should play, so a
   returning visitor never sees a greeting before the skip runs. */
#intro { display: none; }

html[data-intro] #intro {
  position: fixed;
  inset: 0;
  z-index: var(--z-curtain);
  display: grid;
  place-items: center;
  background: var(--color-bg);
  color: var(--color-text);
  will-change: transform;
  animation: intro-curtain var(--duration-intro-curtain) var(--ease-curtain)
             calc(var(--duration-intro-word) * 11) forwards;
}

.intro-stage {
  display: grid;
  place-items: center;
  padding: 0 var(--space-md);
}

.intro-word {
  grid-area: 1 / 1;
  height: clamp(48px, 10vw, 72px);
  width: auto;
  max-width: 100%;
  fill: currentColor;
  opacity: 0;
  animation: intro-word calc(var(--duration-intro-word) + 80ms) var(--ease-out) both;
  animation-delay: calc(var(--i) * var(--duration-intro-word));
}

/* the last greeting holds instead of fading, so the curtain lifts on a word */
.intro-word--last {
  animation-name: intro-word-hold;
}

.intro-defs {
  position: absolute;
  width: 0;
  height: 0;
}

html[data-intro],
html[data-intro] body {
  overflow: hidden;
}

/* While the greeting plays the page waits below the fold and the chrome is
   unlit. When the curtain starts to lift the page rises into place over the
   same beat, and the bar, panel and corner control fade in a moment later.
   Both end where the page normally rests, so dropping the attribute afterwards
   changes nothing on screen. */
html[data-intro="playing"] main {
  transform: translateY(115vh);
}

html[data-intro="playing"] .header-wrapper {
  opacity: 0;
}

html[data-intro="arriving"] main {
  transform: none;
  transition: transform var(--duration-intro-arrive) var(--ease-arrive);
}

html[data-intro="arriving"] .header-wrapper {
  opacity: 1;
  transition: opacity var(--duration-intro-chrome) var(--ease-glide) var(--delay-intro-chrome);
}

@keyframes intro-word {
  0%   { opacity: 0; }
  28%  { opacity: 1; }
  71%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes intro-word-hold {
  0%   { opacity: 0; }
  28%  { opacity: 1; }
  100% { opacity: 1; }
}

@keyframes intro-curtain {
  from {
    transform: translateY(0);
    border-radius: 0;
  }
  to {
    transform: translateY(-100%);
    border-radius: 0 0 50% 50% / 0 0 10vh 10vh;
  }
}

@media (prefers-reduced-motion: reduce) {
  #intro { display: none; }
}

/* ===== MENU BELOW THE DESKTOP BREAKPOINT ===== */
/* Under 1024px there is no room beside the column for the panel, so the menu
   button appears and the same links fill the window instead, sliding in one
   after another from the top and leaving the same way in reverse. The panel
   is painted solid: nothing reads through it. */
@media (max-width: 1023px) {
  .header-menu-btn { display: flex; }

  /* The links start the desktop column's distance down, but that lead-in is
     the first thing to give way in a short window, down to just under the bar,
     so the theme control closing the list stays on screen; Safari's toolbars
     leave most phones too short for the full lead-in. A window too short for
     the links themselves scrolls the panel rather than cutting links off. */
  .side-nav {
    inset: 0;
    display: none;
    padding: 0 var(--space-lg) var(--chrome-floor);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--color-bg);
    opacity: 0;
    pointer-events: none;
  }

  /* The lead-in, as an item of the column: it shrinks before anything else
     does, because the rows cannot go below a finger's height. */
  .side-nav::before {
    content: '';
    flex: 0 1 220px;
    min-height: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-md));
  }

  .side-nav.is-transitioning {
    display: flex;
    transition: opacity var(--duration-normal) ease-in var(--duration-slow);
  }

  .side-nav.is-open {
    opacity: 1;
    pointer-events: auto;
    transition: opacity var(--duration-normal) ease-out;
  }

  .side-nav a,
  .side-nav-theme {
    display: flex;
    align-items: center;
    min-height: var(--space-2xl);
    padding: var(--space-xs) 0;
    font-size: var(--h2-size);
    line-height: var(--leading-tight);
    opacity: 0;
    transform: translateY(-8px);
  }

  /* The glyph grows with the lettering beside it. */
  .side-nav-theme svg {
    width: var(--icon-lg);
    height: var(--icon-lg);
  }

  .side-nav.is-transitioning a,
  .side-nav.is-transitioning .side-nav-theme {
    transition: opacity var(--duration-normal) ease-in, transform var(--duration-normal) ease-in;
  }

  .side-nav.is-open a,
  .side-nav.is-open .side-nav-theme {
    opacity: 1;
    transform: none;
    transition: opacity var(--duration-normal) ease-out, transform var(--duration-normal) ease-out;
  }

  /* Opening runs top to bottom, closing bottom to top, 20ms apart. Each item
     carries its place (--i) and the nav its length (--n), both written by the
     build, so a link added to the nav joins the stagger with no rule here. */
  .side-nav.is-transitioning a,
  .side-nav.is-transitioning .side-nav-theme { transition-delay: calc(40ms + (var(--n) - 1 - var(--i)) * 20ms); }
  .side-nav.is-open a,
  .side-nav.is-open .side-nav-theme { transition-delay: calc(160ms + var(--i) * 20ms); }
}

/* Reduced motion also applies to the shared menu and section reveals,
   including the inline reveal styles set by main.js. */
@media (prefers-reduced-motion: reduce) {
  .section {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .side-nav, .side-nav a, .side-nav-theme {
    transition: none !important;
    transform: none !important;
  }
}

/* ===== WHERE IS ===== */
/* The map fills the window and the nav floats over it, where the home page
   puts it, with no panel or rule of its own; the page starts past it. */
@media (min-width: 1024px) {
  .wi-page main { padding-left: var(--nav-column); }
}

.wi-page main { padding-top: 0; }

/* The page itself does not scroll: only the list does, inside a box the size of
   the window. Landing on a trip is then a scroll position rather than a journey
   down nine thousand pixels, so there is nothing to travel and nothing to hide. */
body.wi-page {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  /* How far the selected card steps out of the column on either side. The list
     reserves exactly this much padding for it, so the breakout lands on the
     list's own edge instead of overflowing it. */
  --wi-breakout: var(--space-md);

  /* The focal band, as a fraction of the viewport: where the selected trip
     comes to rest, and where the map puts the country it names. On a wide
     screen the list and the map sit side by side and both are centred; the
     phone stacks them instead, so the card rides high and the country sits in
     the clear space below it. CSS owns these two numbers and where-is.js reads
     them, so the breakpoint lives in one place. */
  --wi-focus: 0.5;
  --wi-map-focus: 0.5;
}

.wi-scroll {
  position: relative;
  z-index: 1;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.wi-map {
  position: fixed;
  inset: 0;
  z-index: 0;
  /* dvh so the collapsing mobile URL bar cannot leave a gap at the bottom */
  pointer-events: none;
}

@media (min-width: 768px) {
  /* Fade the map out under the timeline so the cards sit on clean ground.
     The stops are fractions of the width and the alpha kept at each, not
     palette colours. where-is.js reads them as well, to fade the city beads
     by hand: they cannot sit under this mask and still blur. */
  body.wi-page {
    --wi-fade-start: 0.38;
    --wi-fade-mid: 0.68;
    --wi-fade-mid-alpha: 0.4;
    --wi-fade-end-alpha: 0.16;
  }

  .wi-map {
    -webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(var(--wi-fade-start) * 100%),
      rgba(0, 0, 0, var(--wi-fade-mid-alpha)) calc(var(--wi-fade-mid) * 100%),
      rgba(0, 0, 0, var(--wi-fade-end-alpha)) 100%);
    mask-image: linear-gradient(to right, #000 0%, #000 calc(var(--wi-fade-start) * 100%),
      rgba(0, 0, 0, var(--wi-fade-mid-alpha)) calc(var(--wi-fade-mid) * 100%),
      rgba(0, 0, 0, var(--wi-fade-end-alpha)) 100%);
  }
}

.wi-map-svg { display: block; width: 100%; height: 100%; }

.wi-world,
.wi-pins,
.wi-markers-plane {
  transition: transform var(--duration-cinematic) var(--ease-glide);
}

.map-graticule {
  fill: none;
  stroke: var(--map-grid);
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
}

.map-country {
  fill: var(--map-land);
  stroke: var(--map-edge);
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
  transition: fill var(--duration-slower) var(--ease-in-out),
              stroke var(--duration-slower) var(--ease-in-out);
}

.map-country.is-visited {
  fill: var(--map-visited-land);
  stroke: var(--map-visited-edge);
  pointer-events: auto;
  cursor: pointer;
}

.map-country.is-visited:hover { fill: var(--map-active-land); }

/* The border traces itself, then stands solid. pathLength="1" on every path
   makes the dash units fractions of the outline, so this needs no measuring.
   vector-effect is off here on purpose: non-scaling-stroke resolves the dash in
   screen space and silently defeats that normalisation. */
.map-country.is-active {
  fill: var(--map-active-land);
  stroke: var(--map-active-edge);
  stroke-width: var(--wi-border-width, 0.1);
  vector-effect: none;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--duration-cinematic) var(--ease-glide),
              fill var(--duration-cinematic) var(--ease-in-out) 200ms,
              stroke var(--duration-normal) var(--ease-in-out);
}

/* No transition on the way in: both classes arrive together, and without this
   the offset eases towards 1 instead of starting there, so the border only ever
   moves a few percent before it is released again. */
.map-country.is-active.is-drawing {
  stroke-dashoffset: 1;
  fill: transparent;
  transition: none;
}

/* Cities sit at the top of the ladder: a glow and a pulse, the only things on
   the map that move on their own. The city itself is a glass bead drawn over
   the map; see .wi-marker. */
.wi-aura { fill: url(#wi-aura); }

.wi-pulse {
  fill: none;
  stroke: var(--map-ink);
  stroke-width: 1.3;
  vector-effect: non-scaling-stroke;
  filter: url(#wi-glow);
  transform-box: fill-box;
  transform-origin: center;
  animation: wi-pulse 2600ms cubic-bezier(0, 0.55, 0.45, 1) infinite;
}

.wi-link {
  fill: none;
  stroke: var(--map-ink);
  stroke-width: 1.1;
  opacity: 0.4;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}

@keyframes wi-pulse {
  from { transform: scale(0.4); opacity: 0.85; }
  to   { transform: scale(3);   opacity: 0; }
}

/* The city markers: glass beads over the map, one per city, the first one
   larger. where-is.js builds them a layer of their own after .wi-map, not
   inside it: a mask on an ancestor walls backdrop-filter off from everything
   behind it, so inside the map's fade the beads would blur nothing. It lays
   the two layers out to match the SVG: the outer one repeats
   preserveAspectRatio, the plane repeats the map's own pan, and each bead
   scales itself back to a constant size on screen. */
.wi-markers {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  transform-origin: 0 0;
}

.wi-markers-plane {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
}

.wi-marker {
  position: absolute;
  width: var(--icon-sm);
  height: var(--icon-sm);
  border-radius: var(--radius-full);
  opacity: var(--wi-fade, 1);
  transform: translate(-50%, -50%) scale(var(--wi-marker-scale, 1));
}

.wi-marker--lead {
  width: var(--icon-md);
  height: var(--icon-md);
}

/* The trips are a flight plan flown on the band: the route runs down the gap
   between the date gutter and the cards, inside the scrolling list, with a dot
   on every trip, and the plane holds the focal line while the trips pass under
   it (flight-plan.js, data-flight="band"). The wrapper is the plan's box, so the
   route and the plane are placed from it. */
.wi-plan {
  position: relative;
  /* The list's breakout, carried by the plan so the list does not hang out of
     the plan's box (which the responsive audit reads as overflow). */
  margin-inline: calc(-1 * var(--wi-breakout));
}

/* A trip's dot, centred on the gap the route runs down: the row's breakout
   padding, the gutter, then half the gap, less half the dot; the row names
   its gutter and gap so the grid and the dot cannot drift apart. The plane
   sits above the cards, which step out over the route when selected. */
.wi-plan .flight-plane {
  z-index: 1;
}

.wi-trip::before {
  content: '';
  position: absolute;
  left: calc(var(--wi-breakout) + var(--wi-gutter) + var(--wi-gap) / 2 - 5px);
  top: 50%;
  margin-top: -5px;
  box-sizing: border-box;
  width: 10px;
  height: 10px;
  border: var(--border-medium) solid var(--color-border);
  border-radius: var(--radius-full);
  background: var(--color-bg);
  transition: background-color var(--duration-slow) var(--ease-in-out),
              border-color var(--duration-slow) var(--ease-in-out);
}

.wi-trip.is-flown::before {
  border-color: var(--color-text);
  background: var(--color-text);
}

.wi-stage {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: 0 var(--space-md);
}

.wi-hero { padding: 13vh 0 var(--space-2xl); }

.wi-title {
  font-family: var(--font-reading);
  font-size: clamp(32px, 6vw, 48px);
  font-weight: var(--font-bold);
  line-height: var(--leading-none);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
  margin: 0;
  /* Every other page opens at the display size, so this one does too. It cannot
     use .hero-name's 72px: the sticky hero column is 324px at 1024 and up, and
     "Where am I going?" needs 650px on one line there. Stacking the tense under
     "Where" fits the widest tense in 266px and gives the page its display tier.
     The break is a <br> in the markup, not wrapping: "am I?" fits beside
     "Where" and "am I going?" does not, so left to itself the hero would change
     height every time the tense swapped. nowrap keeps each line whole. */
  white-space: nowrap;
}

/* Only the tail changes tense, so "Where" holds still while it swaps. The slot
   keeps its own cell so a descender or blur is never clipped. */
.wi-title-slot {
  display: inline-grid;
  overflow: visible;
  padding: 0.14em 0.08em 0.18em;
  margin: -0.14em -0.08em -0.18em;
}

.wi-title-tail {
  grid-area: 1 / 1;
  animation: wi-tense var(--duration-slow) var(--ease-out);
}

@keyframes wi-tense {
  from { opacity: 0; filter: blur(3px); transform: translateY(0.16em); }
  to   { opacity: 1; filter: blur(0);   transform: translateY(0); }
}

.wi-timeline {
  list-style: none;
  /* Deep enough at both ends that the first and last trips can reach the
     middle of the viewport, where the observer band selects them, with extra
     room at the top so the map can rest on home before the first one. */
  /* Padding, not margin: a scroll box leaves the last child's bottom margin out
     of its scroll range, which stranded the last trip short of the band by a
     screenful. Deep enough at both ends for the first and last trips to reach
     the band wherever it ended up: where-is.js clamps it away from the bar on a
     short window and publishes both depths, and the fraction is the fallback
     until it does. */
  padding: var(--wi-lead, calc(var(--wi-focus) * 100vh + 8vh))
           var(--wi-breakout)
           var(--wi-tail, calc((1 - var(--wi-focus)) * 100vh - 8vh));
  padding: var(--wi-lead, calc(var(--wi-focus) * 100dvh + 8vh))
           var(--wi-breakout)
           var(--wi-tail, calc((1 - var(--wi-focus)) * 100dvh - 8vh));
}

.wi-trip {
  --wi-gutter: 52px;
  --wi-gap: var(--space-lg);
  position: relative;
  display: grid;
  grid-template-columns: var(--wi-gutter) minmax(0, 1fr);
  align-items: center;
  gap: var(--wi-gap);
  /* The row reserves the breakout the same way the list does, so the selected
     card lands on the row's edge rather than hanging off it. Padding and the
     negative margin cancel: the columns sit exactly where they did. */
  padding: var(--space-md) var(--wi-breakout);
  margin-inline: calc(-1 * var(--wi-breakout));
  scroll-snap-align: start;
  opacity: 0.26;
  cursor: pointer;
  /* The cards tilt away from the one in focus, so the list reads as a stack
     receding in both directions rather than a flat column that dims. */
  perspective: 1000px;
  transition: opacity var(--duration-slower) var(--ease-in-out);
}

.wi-trip.is-active { opacity: 1; }
.wi-trip:focus-visible { outline: var(--focus-width) solid var(--focus-color); }

.wi-when {
  display: flex;
  flex-direction: column;
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  text-align: right;
}

.wi-month { color: var(--color-text-subtle); }
.wi-trip.is-active .wi-month { color: var(--color-text); }

.wi-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--space-md);
  align-items: baseline;
  padding: var(--space-md);
  border: var(--border-thin) solid transparent;
  border-radius: var(--radius-lg);
  transition: background-color var(--duration-slower) var(--ease-in-out),
              border-color var(--duration-slower) var(--ease-in-out),
              transform var(--duration-slower) var(--ease-glide),
              margin var(--duration-slower) var(--ease-glide);
}

/* Right now: the only row that names itself rather than a date. */
.wi-trip--now .wi-card { border-color: var(--color-border-muted); }

.wi-now-label {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  grid-column: 1 / -1;
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  margin: 0 0 var(--space-xs);
}

.wi-now-dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--map-ink);
  animation: wi-now 3800ms var(--ease-out) infinite;
}

@keyframes wi-now {
  from { box-shadow: 0 0 0 3px var(--color-bg), 0 0 0 0 var(--map-halo); }
  70%  { box-shadow: 0 0 0 3px var(--color-bg), 0 0 0 6px transparent; }
  to   { box-shadow: 0 0 0 3px var(--color-bg), 0 0 0 0 transparent; }
}

.wi-trip.is-before .wi-card { transform: rotateX(10deg) scale(0.96); }
.wi-trip.is-after .wi-card { transform: rotateX(-10deg) scale(0.96); }

/* The selected trip steps out of the column wearing the site's glass, the
   surface every floating control shares; see .header-action-btn. */
.wi-trip.is-active .wi-card {
  border-color: transparent;
  margin: 0 calc(-1 * var(--wi-breakout));
}

.wi-place {
  font-family: var(--font-reading);
  font-size: var(--h3-size);
  font-weight: var(--font-medium);
  line-height: var(--leading-tight);
  color: var(--color-text);
  margin: 0;
}

.wi-dates {
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  text-align: right;
  margin: 0;
  white-space: nowrap;
}

.wi-cities,
.wi-days {
  font-size: var(--text-sm);
  color: var(--color-text-subtle);
  margin: 0;
}

.wi-days { text-align: right; white-space: nowrap; }

@media (min-width: 1024px) {
  .wi-stage {
    display: grid;
    grid-template-columns: minmax(280px, 1fr) minmax(0, 620px);
    gap: var(--space-2xl);
    align-items: start;
  }

  .wi-hero {
    position: sticky;
    top: 15vh;
    margin-top: 15vh;
    padding: 0;
  }

}

/* The stage is one column until the desktop breakpoint, so for this whole
   range the map is behind the list rather than beside it and the card has to
   move out of the way. Below 1024, not below 768: at 768 the nav appears but
   the stage has not split yet, and centring both put the card back on top of
   the country it names. */
@media (max-width: 1023px) {
  body.wi-page {
    --wi-focus: 0.26;
    --wi-map-focus: 0.66;
  }

  /* A trip is selected when it is nearest the band, so a scroll can stop with
     it half a row high and its card behind the bar. Proximity snapping settles
     it onto the band instead, without fighting a reader on their way somewhere
     else. where-is.js publishes where a row's top edge has to land for its
     middle to sit on the band. Stacked only: side by side the card is beside
     the map rather than over it, and free scrolling is the older, calmer
     behaviour there. */
  .wi-scroll {
    scroll-snap-type: y proximity;
    scroll-padding-top: var(--wi-snap, 0px);
  }

}

@media (max-width: 767px) {
  /* The map reads as context on a phone, not as the main event, and its city
     beads with it: they sit outside .wi-map, so they are dimmed on their own.
     The opacity is on each bead, never on their layer, since an ancestor's
     opacity would wall off the backdrop their blur reads. */
  .wi-map,
  .wi-marker { opacity: 0.55; }

  .wi-trip {
    --wi-gutter: 44px;
    --wi-gap: var(--space-md);
  }

  .wi-card {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-sm) var(--space-md);
  }

  .wi-dates,
  .wi-days { text-align: left; }

  body.wi-page { --wi-breakout: var(--space-sm); }
}

@media (prefers-reduced-motion: reduce) {
  .wi-title-tail { animation: none; }
  .wi-now-dot { animation: none; }
  .wi-trip.is-before .wi-card,
  .wi-trip.is-after .wi-card { transform: none; }
  .wi-pulse { animation: none; opacity: 0.5; }
  .map-country.is-active { transition: none; }
}

/* ===== PHOTOGRAPHY (photo.html only) ===== */
/* Photographs run wider than the reading column: edge to edge on a phone, with
   a gutter from a tablet up, and on a wide screen from the side nav's edge to a
   cap. The title keeps a gutter everywhere, and lines up with the grid. */
.gallery-hero {
  padding-inline: var(--space-md);
}

/* --gallery-columns is how many columns fit, set with the breakpoints below;
   js/gallery.js reads it, so the breakpoints live only here. */
.gallery-grid {
  --gallery-columns: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  max-width: var(--container-xl);
}

/* With the script, the tiles move into columns, each photo to the shortest. */
.gallery-grid.is-columns {
  flex-direction: row;
  align-items: flex-start;
}

.gallery-column {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.gallery-link {
  position: relative;
  display: block;
  /* A held finger is how a phone hovers: it lifts the fog (js/fog.js). The
     browser must not answer the hold with its link preview, or lay its grey
     tap flash over the colour. */
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* Monochrome is a presentation choice, not a baked asset: the file stays in
   colour, and the viewer shows it that way. Corners stay square. */
.gallery-link img {
  display: block;
  width: 100%;
  height: auto;
  filter: grayscale(1);
}

/* The fog's canvas lies over the grey photo, drawn by js/fog.js, and lets
   every click through to the link. */
.gallery-fog {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Inset, so the ring is not cut off where a tile meets the edge of a phone. */
.gallery-link:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: calc(-1 * var(--focus-width));
}

.gallery-caption {
  max-width: var(--container-reading);
  padding: var(--space-sm) var(--space-md) 0;
}

/* In columns the caption waits for the viewer, as on Unsplash. */
.gallery-grid.is-columns .gallery-caption {
  display: none;
}

.gallery-empty {
  padding: var(--space-2xl) var(--space-md);
  text-align: center;
  color: var(--color-text-subtle);
}

@media (min-width: 768px) {
  .gallery {
    padding-inline: var(--space-lg);
  }

  .gallery-hero {
    padding-inline: 0;
  }

  .gallery-caption {
    padding-inline: 0;
  }

  .gallery-grid {
    --gallery-columns: 2;
  }
}

@media (min-width: 1024px) {
  .gallery-page main {
    padding-left: var(--nav-column);
  }

  .gallery {
    padding-left: 0;
  }

  .gallery-grid {
    --gallery-columns: 3;
  }
}

/* The viewer: the photo whole and in colour, on the page's own background.
   Only the open dialog is styled, so a closed one stays out of layout. The
   script sizes the frame to the largest the window holds with the arrows and
   the caption under it; the padding keeps the close button, and on a wide
   screen the arrows, off the photo. */
.gallery-viewer[open] {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-sm)) 0;
  background: var(--color-bg);
  color: var(--color-text);
}

html.has-viewer-open,
html.has-viewer-open body {
  overflow: hidden;
}

.gallery-viewer-figure {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.gallery-viewer-frame {
  position: relative;
}

/* Contain, so the photo being left is never stretched to the next one's shape
   in the moment before the next one has loaded. */
.gallery-viewer-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Five lines at most; a longer description scrolls rather than squeezing
   the photo. */
.gallery-viewer-caption {
  width: 100%;
  max-width: var(--container-reading);
  max-height: calc(var(--space-md) + 5 * var(--body-size) * var(--leading-relaxed));
  overflow-y: auto;
  padding: var(--space-md) var(--space-lg) 0;
}

.gallery-viewer-btn[hidden] {
  display: none;
}

/* The close button stands where the menu button does, and is its cross. */
.gallery-viewer-close {
  position: fixed;
  top: var(--chrome-inset);
  right: var(--chrome-inset);
}

.gallery-viewer-close .menu-line:first-child { transform: rotate(45deg); }
.gallery-viewer-close .menu-line:last-child { transform: rotate(-45deg); }

/* On a phone the arrows stand in a row under the photo, clear of it, lined up
   with the close button's edge. The next arrow keeps to the right even when
   the previous one is hidden. */
.gallery-viewer-nav {
  width: 100%;
  display: flex;
  padding: var(--space-md) var(--chrome-inset) 0;
}

.gallery-viewer-step[data-step="1"] {
  margin-left: auto;
}

/* From a tablet up, and on a phone turned sideways, where a row under the photo
   would leave it no height at all, the arrows stand beside it instead. */
@media (min-width: 768px), (orientation: landscape) {
  .gallery-viewer[open] {
    padding-inline: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-md));
  }

  .gallery-viewer-caption {
    padding-inline: 0;
  }

  /* The arrows stand at the window's edges, and the row they leave takes no
     height. */
  .gallery-viewer-nav {
    padding: 0;
  }

  .gallery-viewer-step {
    position: fixed;
    top: 50%;
    translate: 0 -50%;
  }

  .gallery-viewer-step[data-step="-1"] { left: var(--chrome-inset); }
  .gallery-viewer-step[data-step="1"] { right: var(--chrome-inset); }
}

/* A phone or small tablet on its side has its height to spare only for the
   photo. The close button stays clear of it, up in the column the arrows
   stand in. */
@media (max-width: 1023px) and (orientation: landscape) {
  .gallery-viewer[open] {
    padding-block: var(--space-md);
  }
}

/* ===== FEATURED BADGES ===== */
/* Talks and press share one section, so each item says which it is. */

/* ===== NOW PLAYING (index.html only) ===== */
/* A shelf of slipcases, one for each title in now-playing.csv, the newest at the
   left and largest, the rest stepping down and tucking behind it, all turned so
   their spines show, built the way rog.ie builds its shelf of films. A case is
   four faces around a tape, each a span turned into place the way the Where Is
   cards lean, and nothing more: no canvas, no library. The slot around a case
   carries the reveal, which the case cannot: an opacity animation on the case,
   or a button for one, flattens the 3D inside it. Each slot has its own
   perspective, so each case is seen from straight in front of it, and the
   shelf itself is flat: the cases are stacked by number, the newest in front,
   and the case under the pointer in front of them all. now-playing.js sizes
   the cases from the pointer and names the nearest in the pill under it. The
   sides of a case wear its poster, stretched to a smear of its colours, as
   rog.ie's do. */
.vhs-slot {
  perspective: var(--case-perspective);
}

.vhs-stage {
  position: relative;
}

/* The cases start the same step under the heading as every other section's
   content does; the tape slides sideways and takes no room of its own, and
   the pill hangs in the margin under the shelf. Room on the right is for the
   fan to lean into. The whole fan fits the column on every screen; only on the
   very narrowest phone, where the tap-target floor wins, does it scroll, and
   then without a bar, as the Featured strip does. */
.vhs-shelf {
  display: flex;
  align-items: flex-end;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 0 var(--space-md) var(--space-lg);
}

.vhs-shelf::-webkit-scrollbar {
  display: none;
}

/* A slot's --k, set by the build, is its case's scale at rest, the newest
   full and the rest stepping evenly down. --s is the scale now: --k until
   now-playing.js sets it from the pointer's distance, as rog.ie's shelf
   does, so the case under the pointer is full size and the rest fall off
   with their distance from it. The size a scale gives is its square, from
   --case-scale-min up to the newest case's, so the fall-off is gentle near
   the pointer and the far cases hold together small. The size is in the
   layout, so the neighbours make way; a case's place in the stack is its
   scale in hundredths; a smaller case tucks deeper; and the smaller cases
   are the least bit blurred, as things further off are. The blur alone
   eases, as rog.ie's does; the size and the turn follow the hand. */
.vhs-slot {
  --s: var(--k);
  --step: calc(var(--case-scale-min) + var(--s) * var(--s) * (1 - var(--case-scale-min)));
  --w: calc(var(--case-w) * var(--step));
  --h: calc(var(--w) / (var(--aspect-poster)));
  --d: calc(var(--case-depth) * var(--step));
  position: relative;
  z-index: calc(var(--s) * 100);
  width: var(--w);
  height: var(--h);
  flex: none;
  margin: 0 calc(-1 * (var(--case-tuck) + var(--case-tuck-more) * var(--step)) * var(--case-w));
  filter: blur(calc(var(--border-thin) * (1 - var(--step))));
  transition: filter var(--duration-normal) var(--ease-in-out);
}

/* The newest case starts at the column's edge; rog.ie centres its row, so
   the tuck at its ends shows nowhere, and here the row starts at the left. */
.vhs-slot:first-child {
  margin-left: 0;
}

@media (min-width: 768px) {
  .vhs-shelf {
    overflow: visible;
    padding-right: var(--space-xl);
  }
}

/* Cases slide in from the right and settle one after another, once the shelf
   is in view. The slide is a transform and nothing else: an opacity keyframe
   here would flatten the case inside for as long as it held, which with a
   fill of both is for good. The section's own fade-in covers the arrival. */
.vhs-shelf.is-shelved .vhs-slot {
  animation: vhs-shelve var(--duration-cinematic) var(--ease-spring) both;
  animation-delay: calc(var(--i) * var(--duration-fast));
}

@keyframes vhs-shelve {
  from {
    translate: calc(var(--case-w) * 0.8) 0;
    scale: 0.9;
  }
  to {
    translate: 0 0;
    scale: 1;
  }
}

/* A case turns in proportion to its size, the newest the most, swinging
   about a point low and to the left of it. The turn has no transition: the
   script sets the scale straight from the pointer, so the case never lags
   the hand, as rog.ie's do not. At rest nothing moves. */
.vhs-case {
  position: absolute;
  inset: 0;
  display: block;
  color: inherit;
  text-decoration: none;
  transform-style: preserve-3d;
  transform-origin: -25% 80%;
  transform: rotateY(calc(var(--case-tilt) * var(--step)));
}

.vhs-case:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.vhs-face {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--w);
  height: var(--h);
  backface-visibility: hidden;
  border-radius: var(--radius-sm);
}

/* The front carries the poster, with a bite out of its open side: a notch
   low on the right, the shape of the one on rog.ie's sleeves, through which
   the tape's reel shows at rest. */
.vhs-front {
  --bite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 225'%3E%3Cpath d='m150 203.191c0-4.573-1.958-8.929-5.378-11.965l-27.882-24.75c-3.585-3.183-3.585-8.783 0-11.966l27.882-24.749c3.42-3.037 5.378-7.392 5.378-11.966v-117.795h-150v225h150z' fill='%23000'/%3E%3C/svg%3E");
  transform: translateZ(calc(var(--d) / 2));
  background: var(--tape-edge);
  overflow: hidden;
  mask-image: var(--bite);
  mask-size: 100% 100%;
  -webkit-mask-image: var(--bite);
  -webkit-mask-size: 100% 100%;
}

.vhs-front img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A soft light on the poster that follows the pointer over the case nearest
   it, the way a gloss cover catches the room. It is where the script says,
   and nowhere until it does. */
.vhs-front::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--gx) var(--gy), var(--case-ink), transparent 50%);
  opacity: var(--opacity-faint);
}

/* The back is dark plastic, seen from inside through the open side. */
.vhs-back {
  transform: rotateY(180deg) translateZ(calc(var(--d) / 2));
  background: var(--tape-edge);
  backface-visibility: visible;
}

/* The spine, top and bottom wear the poster stretched wide, a smear of its
   own colours, the way the edges of a printed sleeve carry the artwork
   round. The spine is the left face, turned away, with the title running up
   it. The right side is open: there is no face, and what shows in the
   opening is the inside of the back and the tape's own edge. */
.vhs-spine,
.vhs-top,
.vhs-bottom {
  background: var(--poster) 100% 0 / calc(var(--w) * 2.5) 100% no-repeat;
}

.vhs-spine {
  width: var(--d);
  left: calc((var(--w) - var(--d)) / 2);
  transform: rotateY(-90deg) translateZ(calc(var(--w) / 2));
  display: flex;
  align-items: center;
  justify-content: center;
}

.vhs-top,
.vhs-bottom {
  height: var(--d);
  top: calc((var(--h) - var(--d)) / 2);
}

.vhs-top {
  transform: rotateX(90deg) translateZ(calc(var(--h) / 2));
}

.vhs-bottom {
  transform: rotateX(-90deg) translateZ(calc(var(--h) / 2));
}

/* The title runs up the spine, as it does on a shelf. */
.vhs-spine-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  max-height: calc(var(--h) - var(--space-md));
  font-size: calc(var(--text-xs) * var(--step));
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--case-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The tape inside: a cassette's face drawn in site/assets/images/now-playing/
   vhs-tape.svg, black plastic with the moulded grid and one big reel low in
   the middle, where the bite shows it. It nearly fills the sleeve and sits a
   little back from the front. The case nearest the pointer slides it out of
   the open side, on an easing of its own, as rog.ie's tape has. Its edge is
   a strip of black plastic standing across the sleeve's depth at its right
   side, which is what shows in the opening and comes out with it. */
.vhs-tape {
  --tape-w: calc(var(--w) * 0.95);
  --tape-h: calc(var(--h) * 0.97);
  position: absolute;
  left: calc((var(--w) - var(--tape-w)) / 2);
  top: calc((var(--h) - var(--tape-h)) / 2);
  width: var(--tape-w);
  height: var(--tape-h);
  border-radius: var(--radius-sm);
  background: url("/assets/images/now-playing/vhs-tape.svg") center / 100% 100% no-repeat;
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--d) / 10)) translateX(calc(100% * var(--case-eject) * var(--out, 0)));
  transition: transform var(--duration-normal) var(--ease-out);
}

.vhs-slot.is-near {
  --out: 1;
}

.vhs-tape-edge {
  position: absolute;
  top: 0;
  right: 0;
  width: calc(var(--d) * 0.8);
  height: 100%;
  transform-origin: 100% 50%;
  transform: translateZ(calc(var(--d) * -0.4)) rotateY(90deg);
  background: var(--tape-edge);
}

/* The pill under the case under the pointer: its title and its scores, on
   the same glass as the corner buttons, with a caret up to the case. It hangs
   in the shelf's bottom margin, where it covers no poster and comes nowhere
   near the heading. The script sets where it sits; until then it is out of
   the way at the stage's corner. */
.vhs-tip {
  --tip-x: 0px;
  --tip-y: 0px;
  --tip-caret: 0px;
  position: absolute;
  z-index: 1;
  left: var(--tip-x);
  top: var(--tip-y);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-full);
  background: var(--chrome-fill);
  box-shadow: var(--chrome-shadow), inset 0 0 0 var(--border-thin) var(--chrome-edge);
  backdrop-filter: var(--chrome-blur);
  -webkit-backdrop-filter: var(--chrome-blur);
  color: var(--chrome-glyph);
  font-family: var(--font-reading);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration-fast) var(--ease-out), visibility 0s var(--duration-fast);
}

.vhs-tip.is-shown {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.vhs-tip::after {
  content: "";
  position: absolute;
  top: calc(-1 * var(--space-xs));
  left: var(--tip-caret);
  width: var(--space-sm);
  height: var(--space-sm);
  translate: -50% 0;
  rotate: 45deg;
  background: var(--chrome-fill);
  backdrop-filter: var(--chrome-blur);
  -webkit-backdrop-filter: var(--chrome-blur);
}

.vhs-tip-title {
  font-weight: var(--font-medium);
}

.vhs-tip-imdb,
.vhs-tip-tomato {
  margin-left: var(--space-md);
}

.vhs-tip [hidden] {
  display: none;
}

.vhs-icon {
  width: var(--icon-sm);
  height: var(--icon-sm);
  margin-right: var(--space-xs);
  vertical-align: calc(var(--space-xs) / -2);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (prefers-reduced-motion: reduce) {
  .vhs-shelf.is-shelved .vhs-slot {
    animation: none;
  }

  .vhs-slot,
  .vhs-tape,
  .vhs-tip {
    transition: none;
  }
}

/* ===== HAZE CHASER (project/haze-chaser.html only) =====
   Two views side by side, with shared controls. Sizes, spacing, colours and
   timing come from the design tokens. */
body.hz-page {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;   /* a sideways swipe is the page's, not the browser's back */
  --chrome-wash: 0px;          /* the bar's wash would dim the headline; the map runs to the top */
  --row-y: max(var(--space-md), env(safe-area-inset-bottom));
  --row-h: 44px;               /* its capsules' height: a fingertip; no spacing token is 44px */
  --hz-map-control-size: 44px;
}

/* The two views, stacked in place and crossfaded. */
.hz-page main.hz-views {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: flex;
  padding: 0;
  touch-action: none;
  transition: opacity var(--duration-slower) var(--ease-in-out) var(--duration-normal),
              visibility var(--duration-slower) var(--duration-normal);
  width: 100%;
}

.hz-view {
  overflow: hidden;
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 1;
  visibility: inherit;
  transition: opacity 280ms ease-in-out, visibility 0s;
}

/* The map view keeps Where Is's bearing: the map fills the window, the menu
   floats over it, and the headline stands at the left, clear of the header. */
#hz-map {
  position: absolute;
  inset: 0;
}

/* The headline stands above the island, which is framed in the room beneath
   it (see padding in haze-chaser.js); a band too long for one line on a phone
   or a tablet takes two. */
.hz-hero {
  /* Across the desktop breakpoint, where the menu's column comes and goes, the
     headline and the row glide to their new places rather than jump. */
  /* Navigation and headline sit directly over the map, without tint backdrops. */
  position: absolute;
  top: 0;
  left: 0;
  padding: max(13vh, calc(var(--chrome-inset) + var(--chrome-size) + var(--space-sm))) var(--space-md) 0;
  pointer-events: none;
  transition: left var(--duration-slow) var(--ease-glide), padding-top var(--duration-slow) var(--ease-glide);
  isolation: isolate;
  z-index: 2;
}

.hz-title {
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-reading);
  font-size: var(--h1-size);
  font-weight: var(--font-bold);
  line-height: var(--leading-none);
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  text-wrap: balance;
}

.hz-meta {
  margin: var(--space-xs) 0 0;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* The reading's time, in either view, underlined as a live clock is, after a
   Pinterest pin's underline: a rounded line in the time's own colour draws
   out slowly from its middle to its full width and starts again. */
.hz-now {
  position: relative;
}

.hz-now::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: var(--border-medium);
  border-radius: var(--radius-full);
  background: currentColor;
  scale: 0 1;
  animation: hz-now calc(var(--duration-cinematic) * 2) var(--ease-in-out) infinite;
}

@keyframes hz-now {
  to { scale: 1 1; }
}

.hz-view[inert] .hz-now::after {
  animation-play-state: paused;   /* the view off screen draws nothing */
}

/* Each zone on the map: its compass mark and letter, then its counter, in one row. */
.hz-zone {
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* A zone whose centre has left the view as the reader zooms in on another, or
   lies under the page's own elements. */
.hz-zone.is-off {
  visibility: hidden;
}

.hz-zone i {
  font: var(--font-bold) var(--text-sm)/var(--leading-none) var(--font-ui);
  font-style: normal;
  letter-spacing: var(--tracking-widest);
  color: var(--color-text);
  text-shadow: 0 0 6px var(--color-bg), 0 0 12px var(--color-bg);
}

/* The region the page speaks for: its counter wears a ring in the text colour,
   on the map and in the readings alike, so each says which figure the
   headline is about. A gap in the page's colour parts the ring from the black
   card, so it reads by day as at night. */
.hz-zone.is-home .hz-counter,
#hz-big .hz-counter {
  box-shadow: 0 0 0 var(--border-medium) var(--color-bg),
              0 0 0 calc(var(--border-medium) * 2) var(--color-text),
              0 3px 8px var(--hz-card-shadow);
}

/* The map's optional controls, the compass and zoom in and out, in a column
   at the map's right above the row, in the capsules the globe's controls wear. */
.hz-map-controls {
  bottom: calc(var(--row-y) + var(--row-h) + var(--space-md));
  display: flex;
  flex-direction: column;
  position: absolute;
  right: max(var(--space-md), env(safe-area-inset-right));
  gap: 10px;
  align-items: center;
  z-index: 3;
}

.hz-map-controls[hidden] {
  display: none;
}

/* Where the reader is, when the browser says: a small ink dot on a glass bead
   with Where Is's glow and pulse round it, a soft aura in the map's halo and a
   ring in the map's ink, itself glowing, swelling and fading on the same curve.
   The halo and the ink are the map's own tokens, so the dot is monochrome in
   either theme. It is never covered: the labels ease aside from it (it is an
   obstacle to declutter, the whole of its aura), and it stands over them. */
.hz-you {
  position: relative;
  z-index: 2;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  background: radial-gradient(circle, var(--map-halo), transparent 70%);
  pointer-events: none;
}

.hz-you::after {
  content: '';
  position: absolute;
  inset: 13px;
  border-radius: var(--radius-full);
  background: var(--color-text);
  box-shadow: 0 0 0 5px var(--chrome-fill), 0 0 0 6px var(--chrome-edge),
              0 0 14px 6px var(--map-halo), var(--chrome-shadow);
}

.hz-you::before {
  content: '';
  position: absolute;
  inset: 10px;
  border: 1.3px solid var(--map-ink);
  border-radius: var(--radius-full);
  box-shadow: 0 0 6px var(--map-halo), inset 0 0 4px var(--map-halo);
  animation: wi-pulse 2600ms cubic-bezier(0, 0.55, 0.45, 1) infinite;
}

/* The readings view, after psi.sg: the measure, one large figure, its band,
   and a chip for each region, between the header and the controls.
   A window too short for all of it scrolls, up and down only. */
.hz-view-read {
  display: flex;
  padding: calc(var(--chrome-inset) + var(--chrome-size)) var(--space-md)
           calc(var(--row-y) + var(--row-h) + var(--space-lg));
  overflow-y: auto;
  touch-action: pan-y;
}

.hz-read {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  margin: auto;   /* centred while it fits, and from the top once it does not */
  text-align: center;
}

.hz-read-measure {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  justify-content: center;
}

/* The readings label the regions as the map does, one shape on both pages:
   the compass mark and letter, then the counter, in one row on no capsule,
   the reader's region ringed. The large figure is that label at the
   headline's size, as the band above it is: the page has two sizes of type,
   the headline's for the reader's region and its band, and the text's for
   everything else (on a phone the small counters a size under it). */
.hz-big {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
  color: var(--color-text);
}

.hz-big i {
  font: var(--font-bold) var(--text-sm)/var(--leading-none) var(--font-ui);
}

.hz-read-band {
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-reading);
  font-size: var(--h1-size);
  font-weight: var(--font-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

.hz-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

/* Each other region is the map's label, lifted off the map, and a button
   that makes it the reader's. A button gets the arrow, not the hand a link
   gets, so it asks. The region the large figure shows has no chip; it would
   only say it again. */
.hz-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
  padding: var(--space-xs);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--color-text);
  font: var(--font-bold) var(--text-sm)/var(--leading-none) var(--font-ui);
  cursor: pointer;
  transition: scale var(--duration-fast) var(--ease-out);
}

.hz-chip[hidden] {
  display: none;
}

.hz-chip:active {
  scale: 0.96;
}

.hz-chip:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Before each region's letter, a compass mark in red: a triangle pointing the
   region's way, as a compass rose marks north, and a dot for the centre. On the
   map's labels and the readings' chips, large and small. At night it is lit in
   warm amber. */
.hz-zone i,
.hz-chip i,
.hz-big i {
  display: inline-flex;
  align-items: center;
  gap: .3em;
  font-style: normal;
  letter-spacing: var(--tracking-widest);
}

.hz-dir {
  flex: none;
  width: .55em;
  height: .55em;
  overflow: visible;
  fill: var(--hz-compass);
}

html.dark .hz-dir {
  fill: var(--hz-night-light);
  filter: none;
}

/* Counters: split-flap cards, as a departure board's, in black and
   cream. The figures are the text's size on the map, the chips and the
   readings view's large figure, a size smaller on a phone; a card is as tall as its
   figure needs, and each counter is 16:9 at the outer edge of its ring: three
   cards, two gaps of a twelfth of a card's height, a frame of an eighth, and
   the ring, which keeps its width at every size. */
.hz-counter {
  --digit: var(--text-sm);
  --cell-h: calc(var(--digit) / .65);
  --cell-w: calc((16 / 9 * (var(--cell-h) * 1.25 + 2 * var(--border-thin)) - 2 * var(--border-thin) - var(--cell-h) * 5 / 12) / 3);
  display: inline-flex;
  gap: calc(var(--cell-h) / 12);
  padding: calc(var(--cell-h) / 8);
  border-radius: calc(var(--cell-h) / 12);
  background: var(--hz-card-frame);
  box-shadow: 0 0 0 var(--border-thin) var(--hz-card-edge), 0 3px 8px var(--hz-card-shadow);
}

/* A card: two fixed halves, and two leaves that turn over on the hinge between
   them; haze-chaser.js turns the leaves. The upper half is lit from above, the
   lower a shade darker, as a board's cards are. Each half shows its half of the
   digit by clipping the whole one. */
.hz-card {
  position: relative;
  width: var(--cell-w);
  height: var(--cell-h);
  perspective: calc(var(--cell-h) * 3);
}

.hz-half,
.hz-leaf {
  position: absolute;
  left: 0;
  right: 0;
  height: 50%;
  clip-path: inset(0);
  backface-visibility: hidden;
}

.hz-half.top,
.hz-leaf.top {
  top: 0;
  transform-origin: 50% 100%;
  border-radius: calc(var(--cell-h) / 16) calc(var(--cell-h) / 16) 0 0;
  background: linear-gradient(var(--hz-card-upper-top), var(--hz-card-upper-bottom));
}

.hz-half.bottom,
.hz-leaf.bottom {
  bottom: 0;
  transform-origin: 50% 0;
  border-radius: 0 0 calc(var(--cell-h) / 16) calc(var(--cell-h) / 16);
  background: linear-gradient(var(--hz-card-lower-top), var(--hz-card-lower-bottom));
}

.hz-leaf {
  z-index: 2;
}

/* The lower leaf waits edge-on to the hinge, and out of sight, until it turns. */
.hz-leaf.bottom {
  transform: rotateX(90deg);
  visibility: hidden;
}

.hz-half > span,
.hz-leaf > span {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--cell-h);
  text-align: center;
  color: var(--hz-digit);
  font: 700 var(--digit)/var(--cell-h) 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-variant-numeric: tabular-nums;
}

.hz-half.top > span,
.hz-leaf.top > span {
  top: 0;
}

.hz-half.bottom > span,
.hz-leaf.bottom > span {
  bottom: 0;
}

/* The hinge: a fine dark line across the middle of the card. */
.hz-card::after {
  content: '';
  position: absolute;
  z-index: 3;
  left: 0;
  right: 0;
  top: 50%;
  height: max(1px, calc(var(--cell-h) / 40));
  translate: 0 -50%;
  background: var(--hz-card-frame);
}

/* Identical neutral counters in both views; pure white night figures, no glow. */
html.dark .hz-card span {
  color: #fff;
  text-shadow: none;
}

/* The shared controls, in both views: refresh at the left, the switch
   between the two views in the middle, and at the right the sources and the
   map's credit behind one (i), each in
   the glass capsule the globe's controls wear (see CHROME CAPSULES). Only the
   capsules take the pointer; the map shows through the rest of the row. */
.hz-row {
  position: fixed;
  z-index: 2;
  bottom: var(--row-y);
  pointer-events: none;
  transition: left var(--duration-slow) var(--ease-glide);
  left: max(var(--space-md), env(safe-area-inset-left));
  right: max(var(--space-md), env(safe-area-inset-right));
}

.hz-credit,
.hz-nav {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  font: var(--text-sm)/var(--leading-none) var(--font-reading);
  color: var(--chrome-glyph);
  white-space: nowrap;
}
/* The glass is drawn at the size of its words: a pill --space-lg tall, its
   ends half that beyond the words, as the site's other pills are in
   proportion to theirs. On a touch screen the box around it stays a
   finger's height, so the pill is slim and still easy to press. */
.hz-credit::before,
.hz-nav::before {
  content: '';
  position: absolute;
  z-index: -1;
}

@keyframes hz-turn {
  to { rotate: 360deg; }
}

/* The shared capsule: the other view and the action to change the period,
   each a fingertip wide. */
.hz-nav {
  grid-column: 2;
  gap: 0;
  padding: 0 calc(var(--space-xs) / 2);
  box-sizing: border-box;
}

.hz-nav a,
.hz-period {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  height: 100%;
  text-decoration: none;
  transition: opacity var(--duration-normal) var(--ease-in-out);
}

.hz-nav svg {
  width: 1.4em;
  height: 1.4em;
}

.hz-nav span {
  font-variant-numeric: lining-nums;
}

/* The focus ring is drawn round the pill's own shape, not the taller button. */
.hz-nav a::before,
.hz-period::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  translate: 0 -50%;
}

.hz-nav a:focus-visible,
.hz-period:focus-visible {
  outline: none;
}

.hz-nav a:focus-visible::before,
.hz-period:focus-visible::before {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.hz-credit {
  position: relative;
  flex: none;
}

/* The whole round GrabMaps chip is its link target. */
.hz-credit > a {
  all: unset;
  gap: var(--space-sm);
  cursor: pointer;
}

.hz-credit > a:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Reduced motion keeps view changes and refresh progress still. */
@media (prefers-reduced-motion: reduce) {
  .hz-page main.hz-views,
  .hz-hero {
    transition: none;
  }

  .hz-row {
    transition: none;
  }

  .hz-you::before {
    animation: none;
    opacity: 0.5;
  }

  .hz-now::after {
    animation: none;
    scale: none;
  }
}

@media (min-width: 1024px) {
  /* The headline stands on the wordmark's line, in a line box the wordmark's
     height, so the two share a baseline; the map has the room beneath it. */
  .hz-hero {
    left: var(--nav-column);
    padding-top: var(--chrome-inset);
  }
  .hz-title {
    line-height: var(--leading-normal);
  }
  .hz-view-read {
    padding-left: var(--nav-column);
  }
  .hz-row { left: calc(var(--nav-column) + max(var(--space-md), env(safe-area-inset-left))); }
}

/* Keep the headline and readings compact on a phone. */
@media (max-width: 767px) {

  .hz-read {
    gap: var(--space-md);
  }

  /* The four chips in one row: on a phone the small counters, the map's and
     the chips', are a size smaller, and a chip holds its parts closer. */
  .hz-zone .hz-counter,
  .hz-chip .hz-counter,
  .hz-big .hz-counter {
    --digit: var(--text-xs);
  }

  .hz-chips {
    flex-wrap: nowrap;
    margin-top: var(--space-sm);
  }

  .hz-chip {
    gap: var(--space-xs);
    padding-inline: 0;
  }
}

/* On the narrowest phones the capsules hold their words more closely, so the
   three still fit in one row. */

/* On a short window, keep the readings and chips in two compact rows. */
@media (max-height: 539px) {
  .hz-view-read {
    padding-bottom: calc(var(--row-y) + var(--row-h) + var(--space-sm));
  }

  .hz-read {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: var(--space-lg);
    row-gap: var(--space-sm);
  }

  .hz-chips {
    flex-basis: 100%;
    margin-top: 0;
  }
}

/* The narrowest phones have no room for four across: two by two, their
   compass marks in line. */
@media (max-width: 359px) {
  .hz-chips {
    display: grid;
    grid-template-columns: repeat(2, auto);
    justify-content: center;
    justify-items: start;
    gap: 0 var(--space-md);
  }
}

/* The reading chips are something a thumb has to land on: they take the 48px
   floor the nav's links do. */
@media (pointer: coarse) {
  .hz-chip {
    min-height: var(--space-2xl);
  }
}

/* The third capsule button offers the other reading period. */
.hz-period {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
}

@media (max-width: 767px) {
  .hz-view-read {
    padding-bottom: calc(var(--row-y) + var(--row-h) * 2 + var(--space-lg));
  }

  .hz-nav {
    grid-column: 1 / -1;
    grid-row: 1;
    justify-self: center;
  }

  .hz-credit {
    justify-self: end;
  }
}

/* A short phone cannot stack zoom buttons between the header and two control rows. */
@media (max-width: 767px) and (max-height: 539px) {
  .hz-map-controls {
    flex-direction: row;
    bottom: calc(var(--row-y) + var(--row-h) * 2 + var(--space-sm));
  }
}

/* Map progress belongs to its view and never covers the shared controls. */
.hz-map-status {
  z-index: 1;
  pointer-events: auto;
}

.hz-map-status button {
  min-height: var(--space-2xl);
  min-width: var(--space-2xl);
  font: inherit;
  background: transparent;
  border: var(--border-thin) solid currentColor;
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-sm);
}

/* The legend shares the control capsules' glass, size and baseline. */
.hz-haze-legend {
  position: relative;
  z-index: 2;
  isolation: isolate;
  left: auto;
  bottom: auto;
  display: flex;
  color: var(--chrome-glyph);
  font: var(--text-xs)/var(--leading-normal) var(--font-ui);
  justify-content: flex-end;
}
.hz-haze-heading, .hz-haze-note { display: none; }
.hz-haze-bands {
  display: flex;
  overflow: hidden;
  flex: 1;
  min-width: 0;
  width: auto;
  height: var(--row-h);
  margin: 0;
  padding: 1px;
  box-sizing: border-box;
  border-radius: var(--radius-xl);
  background: var(--chrome-fill);
  box-shadow: var(--chrome-shadow), inset 0 0 0 var(--border-thin) var(--chrome-edge);
  backdrop-filter: var(--chrome-blur);
  -webkit-backdrop-filter: var(--chrome-blur);
}
.hz-haze-band {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text);
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--color-text) var(--band-start), var(--color-bg)),
    color-mix(in srgb, var(--color-text) var(--band-end), var(--color-bg)));
}
.hz-haze-band + .hz-haze-band { box-shadow: inset var(--border-thin) 0 var(--chrome-edge); }
.hz-haze-band.is-dense { color: var(--color-bg); }
.hz-haze-band > .hz-haze-range {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  font: var(--text-xs)/1 var(--font-ui);
  text-align: center;
  font-variant-numeric: tabular-nums;
  padding-inline: var(--space-xs);
}
.hz-haze-range span { display: block; flex: none; white-space: nowrap; }
.hz-wind-speed { white-space: nowrap; display: inline-flex; align-items: center; gap: .3em; flex: none; }
.hz-wind-canvas { position: absolute; inset: 0; pointer-events: none; }

.hz-navigation {
  grid-column: 2;
  display: flex;
  justify-self: center;
  margin-inline: auto;
}
.hz-navigation > * { flex-shrink: 0; }
.hz-time-refresh {
  border: 0; padding: 0; background: none; color: inherit; font: inherit;
  display: inline-flex; align-items: center; gap: var(--title-link-arrow-gap);
  min-height: var(--space-lg); vertical-align: middle; cursor: pointer; pointer-events: auto;
  flex: none;
}
.hz-time-refresh svg {
  width: var(--title-link-arrow-size); height: var(--title-link-arrow-size);
}
.hz-time-refresh:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.hz-time-refresh::after { bottom: calc(50% - .6em - var(--space-xs)); }
@media (pointer: coarse) {
  .hz-time-refresh { min-height: var(--space-2xl); min-width: var(--space-2xl); justify-content: center; }
}

/* The same readable scale and glass switcher on every display. */
.hz-meta, .hz-read-measure {
  display: flex;
  align-items: center;
  gap: .3em;
  white-space: nowrap;
  letter-spacing: normal;
}
.hz-meta-dot { flex: none; }
.hz-meta.has-refresh-error .hz-time-refresh, .hz-read-measure.has-refresh-error .hz-time-refresh {
  color: var(--color-text);
}
@media (max-width: 1199px) {
  .hz-navigation { align-items: center; }
}
/* Safe-area padding supplements the site's existing header spacing. */
@media (max-width: 1023px) {
  .hz-hero {
    padding-left: max(var(--space-md), env(safe-area-inset-left));
    padding-top: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-sm) + env(safe-area-inset-top));
  }
  .hz-view-read {
    padding-inline: max(var(--space-md), env(safe-area-inset-left)) max(var(--space-md), env(safe-area-inset-right));
  }
  html.has-menu-open .hz-page .hz-views, html.has-menu-open .hz-page .hz-row {
    visibility: hidden;
    pointer-events: none;
  }
  html.has-menu-open .hz-page .hz-row { display: none; }

  /* The open menu scrolls below the header; links never pass under its hit areas. */
  .hz-page .side-nav {
    top: calc(var(--chrome-inset) + var(--chrome-size) + var(--space-sm) + env(safe-area-inset-top));
    padding-top: var(--space-sm); /* Keep the entrance animation inside the scrolling panel. */
  }
  .hz-page .side-nav::before { display: none; }
}
@media (max-width: 359px) {
  .hz-meta, .hz-read-measure { gap: .2em; }
}
/* Transparent header space must not intercept the timestamp or retry action. */
.hz-page .floating-header { pointer-events: none; }
.hz-page .floating-header > * { pointer-events: auto; }

/* Relative bands carry their scale name visibly; each gradient follows its values. */
/* Shorter NEA caption keeps its ruler at the same edge as Relative. */
.hz-haze-heading {
  font: 11px/14px var(--font-ui);
  text-align: center;
  color: #fff;
  display: inline-flex;
  align-items: center;
  gap: .35em;
  justify-content: flex-end;
  min-width: 0;
}
.hz-page[data-readings="loading"] .hz-zone { visibility: hidden; }
.hz-map-status, .hz-data-status {
  position: relative;
  top: auto;
  left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  max-width: 280px;
  padding: 6px 10px;
  box-sizing: border-box;
  border: var(--border-thin) solid var(--chrome-edge);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-bg) 78%, transparent);
  backdrop-filter: blur(14px);
  color: var(--color-text-muted);
  font: var(--text-xs)/1.5 var(--font-ui);
}
.hz-map-status[hidden], .hz-data-status[hidden] { display: none; }
.hz-map-status[data-state="loading"]::before, .hz-data-status[data-state="loading"]::before {
  content: '';
  width: 10px;
  height: 10px;
  flex: none;
  border: var(--border-thin) solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: hz-turn 1s linear infinite;
}
.hz-map-status[data-state="ready"]::before, .hz-data-status[data-state="ready"]::before { content: '✓'; }
.hz-map-status button { border-color: var(--chrome-edge); color: var(--color-text); }
.hz-map-status-message { display: block; }
.hz-time-refresh.is-busy::after { opacity: 1; animation: hz-now 1s ease-in-out infinite alternate; }
.hz-time-refresh.is-busy svg { animation: none; opacity: .7; }
@media (max-width: 767px) and (max-height: 539px) {
  .hz-map-status {
    position: fixed;
    top: 18px;
    left: calc(var(--chrome-inset) + var(--chrome-size) + 12px);
    margin: 0;
    max-width: calc(100vw - 180px);
  }
  .hz-map-status-message { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hz-map-status::before, .hz-data-status::before, .hz-time-refresh.is-busy::after { animation: none; }
}
/* Navigation and branding are ready on the first frame, including a refresh. */
.hz-page .header-logo, .hz-page .header-action-btn { animation: none; }
body.hz-page:not([data-view="read"]) .hz-haze-legend[hidden] {
  display: flex;
  visibility: hidden;
  pointer-events: none;
}
@media (max-width: 767px) and (min-height: 540px) { .hz-title { min-height: 2lh; } }

/* When the map has room, the scale label, scale and view controls share a row. */
@media (min-width: 1200px) {
  .hz-navigation { align-items: center; }
  .hz-haze-heading { text-align: left; }
}

/* A native SVG streamline replaces the wind word without adding another control. */
.hz-wind-icon { color: var(--color-text); width: 22px; height: 18px; flex: none; overflow: hidden; }
.hz-wind-trails path { stroke-dasharray: 45 55; }
.hz-wind-trails path:nth-child(2) { animation-delay: -.4s !important; }
.hz-wind-trails path:nth-child(3) { animation-delay: -.8s !important; }
.hz-wind-speed.is-flowing .hz-wind-trails path { animation: hz-wind-flow var(--wind-cycle) linear infinite; }
@keyframes hz-wind-flow { to { stroke-dashoffset: -100; } }
.hz-view[inert] .hz-wind-trails path { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .hz-wind-speed.is-flowing .hz-wind-trails path { animation: none; } }

/* The scale caption stays outside the glass and follows the theme's full ink. */
html.light .hz-haze-heading { color: #000; }

/* Optional map controls reuse the /work globe's exact chrome surface. */
.hz-map-controls button {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--chrome-glyph);
  cursor: pointer;
  width: var(--hz-map-control-size);
  height: var(--hz-map-control-size);
  min-height: var(--hz-map-control-size);
}
.hz-map-controls button:hover { color: var(--color-text); }
.hz-control-group button + button { border-top: var(--border-thin) solid var(--chrome-edge); }
.hz-map-controls svg { display: block; }
.hz-map-controls .hz-compass { background: var(--chrome-fill); }
.hz-compass-needle { transform-origin: 16px 16px; }

.hz-map-controls[data-layout="compact"] { flex-direction: row; }

#hz-controls-toggle {
  display: flex;
  align-items: center;
  font: inherit;
  border: 0;
  cursor: pointer;
  width: var(--hz-map-control-size);
  height: var(--hz-map-control-size);
  min-height: var(--hz-map-control-size);
  padding: 0;
  justify-content: center;
  border-radius: 50%;
  background: var(--chrome-fill);
}

/* Primary map actions share one desktop baseline; mobile keeps two calm rows. */
.hz-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.hz-utilities { display: flex; align-items: center; flex: none; }
#hz-controls-toggle svg { width: var(--icon-md); height: var(--icon-md); }
#hz-controls-toggle:disabled { opacity: .35; cursor: default; }
@media (max-width: 1199px) {
  .hz-row { display: grid; grid-template-columns: 1fr; gap: var(--space-xs); }
  .hz-navigation { grid-column: 1; grid-row: 1; }
  .hz-utilities { grid-column: 1; grid-row: 2; justify-content: space-between; width: 100%; }
}
@media (max-width: 1199px) and (max-height: 539px) and (min-width: 540px) {
  .hz-row { display: flex; align-items: flex-end; }
  .hz-utilities { width: auto; }
}

/* Map/read/period use the grouped glass treatment, without duplicate map buttons. */
.hz-nav { height: 44px; border-radius: var(--radius-xl); }
.hz-nav::before { inset: 0; height: 100%; translate: none; border-radius: var(--radius-xl); }
.hz-nav .hz-period { border-left: var(--border-thin) solid var(--chrome-edge); }
.hz-nav a::before, .hz-period::before { height: calc(100% - var(--space-sm)); border-radius: var(--radius-lg); }

/* Only the destination view is an action; keep two generous, equal hit areas. */
.hz-nav a[aria-current="page"] { display: none; }
.hz-haze-heading svg { width: 1em; height: 1em; flex: none; transform: rotate(-45deg); }

/* Crossfade the two views in place; keep footer anchors stable during the handoff. */
.hz-view[inert] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 280ms ease-in-out, visibility 0s 280ms;
}
.hz-views.is-dragging .hz-view { transition: none; }
body[data-view="read"] .hz-haze-legend { display: flex; }
body[data-view="read"] #hz-controls-toggle { display: flex; visibility: hidden; }
@media (prefers-reduced-motion: reduce) { .hz-view, .hz-view[inert] { transition: none; } }

/* Equal visible glass heights, not just equal transparent hit areas. */
.hz-credit::before { inset: 0; height: 100%; translate: none; }
.hz-time-refresh svg { stroke-width: 2.5; opacity: 1; }

/* Reserved footer space must not block the reading cards behind it. */
.hz-row > .hz-navigation, .hz-row > .hz-utilities { pointer-events: none; }
.hz-nav, .hz-haze-legend, .hz-utilities > * { pointer-events: auto; }
body[data-view="read"] .hz-haze-legend, body[data-view="read"] #hz-controls-toggle { pointer-events: none; }
/* Compact desktop chrome leaves the map in focus; touch retains full targets. */
.hz-nav { width: 120px; }
.hz-nav a, .hz-nav .hz-period { width: 58px; }
.hz-navigation, .hz-utilities { gap: var(--space-sm); }
@media (min-width: 1200px) and (pointer: fine) {
  body.hz-page { --row-h: 32px; }
  .hz-haze-legend, .hz-nav { height: 32px; }
  .hz-nav, .hz-nav::before { border-radius: var(--radius-lg); }
  #hz-controls-toggle svg { width: 18px; height: 18px; }
  .hz-haze-bands { border-radius: var(--radius-lg); }
}

/* Caption and scale stay side by side even on a 320px phone. */
.hz-haze-legend { height: 44px; flex-direction: row; align-items: center; gap: 6px; }
.hz-haze-heading { width: auto; white-space: nowrap; flex: none; }
.hz-credit { padding: 0; width: 44px; height: 44px; }
.hz-credit > a { width: 100%; justify-content: center; }
#hz-credit-name { display: flex; align-items: center; }
#hz-credit-name img { display: block; width: 32px; height: 32px; object-fit: contain; border-radius: 0; filter: none; }
@media (min-width: 1200px) and (pointer: fine) {
  .hz-haze-legend { height: 32px; }
  .hz-credit { width: 32px; height: 32px; }
  #hz-credit-name img { width: var(--icon-lg); height: var(--icon-lg); }
}

/* Reading uses fixed NEA bands; map keeps observed relative quartiles. */
body[data-view="read"] .hz-haze-legend { visibility: visible; pointer-events: auto; }
/* Revealed controls match their launcher, including touch target sizing. */
.hz-control-group { overflow: hidden; width: var(--hz-map-control-size); border-radius: var(--radius-xl); }
.hz-map-controls .hz-compass svg {
  width: calc(var(--hz-map-control-size) - var(--space-sm));
  height: calc(var(--hz-map-control-size) - var(--space-sm));
}
.hz-map-controls svg { width: var(--icon-md); height: var(--icon-md); }
@media (min-width: 1200px) and (pointer: fine) {
  body.hz-page { --hz-map-control-size: 32px; }
  .hz-control-group { border-radius: var(--radius-lg); }
}
/* Give longer NEA ranges their natural width instead of squeezing five equal cells. */
#hz-haze-legend[data-scale="nea"] .hz-haze-band { flex: 1 0 auto; min-width: max-content; }
/* Short landscape uses one footer row, leaving reading cards clear above it. */
@media (max-height: 539px) and (min-width: 560px) and (max-width: 1199px) {
  .hz-row { gap: var(--space-sm); align-items: center; }
}

/* The caption uses only the space its text needs; longer NEA ranges get the rest. */
.hz-haze-legend { width: min(328px, calc(100vw - 32px - env(safe-area-inset-left) - env(safe-area-inset-right))); }
html.light #hz-credit-name img { filter: brightness(0); }
/* One theme-aware ink for every footer action, regardless of active state. */
.hz-nav a, .hz-nav .hz-period, #hz-controls-toggle, #hz-controls-toggle:hover {
  color: var(--chrome-glyph);
  opacity: 1;
}
/* The launcher's icon stays at full strength; the globe's buttons dim theirs. */
#hz-controls-toggle svg { opacity: 1; }
/* Attribution is now an icon, so its chrome matches the circular control launcher. */
.hz-credit, .hz-credit::before, .hz-credit > a { border-radius: 50%; }

/* Stacked footers place the shorter action row above the longer scale.
   Tablet-width layouts align the utilities with the lowest row; narrow phones
   keep them beside the upper actions instead of squeezing the range labels. */
@media (max-width: 1199px) {
  .hz-navigation { flex-direction: column-reverse; }
}
@media (max-width: 1199px) and (min-width: 480px) and (min-height: 540px), (max-width: 479px) {
  .hz-row {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) 44px;
    gap: var(--space-sm);
    align-items: center;
  }
  .hz-row > .hz-navigation, .hz-row > .hz-utilities { display: contents; }
  .hz-nav { grid-column: 2; grid-row: 1; justify-self: center; }
  .hz-haze-legend { grid-column: 2; grid-row: 2; justify-self: center; width: min(328px, 100%); }
  #hz-controls-toggle { grid-column: 1; grid-row: 2; }
  .hz-credit { grid-column: 3; grid-row: 2; }
}
@media (max-width: 479px) {
  .hz-haze-legend { grid-column: 1 / -1; }
  #hz-controls-toggle, .hz-credit { grid-row: 1; }
}
@media (max-width: 1199px) and (max-height: 539px) and (min-width: 540px) {
  .hz-navigation { flex-direction: row; }
  .hz-haze-legend { width: calc(100vw - 265px); }
}

/* The relative legend samples the cloud’s continuous tone curve. */
.hz-page { --hz-field-ink: #fff; }
html.light .hz-page { --hz-field-ink: #171717; }

/* GrabMaps is a direct link; short-screen tools stand independently. */
.hz-credit > a { display: flex; align-items: center; height: 100%; color: inherit; text-decoration: none; }
.hz-map-controls[data-layout="compact"] .hz-control-group { display: flex; width: auto; }
.hz-map-controls[data-layout="compact"] .hz-control-group button + button {
  border-top: 0;
  border-left: var(--border-thin) solid var(--chrome-edge);
}
.hz-page.hz-tools-over-hero .hz-hero,
.hz-page.hz-tools-over-hero #hz-loader { visibility: hidden; }

/* A lightweight dot-field placeholder reveals the prepared map as one surface. */
.hz-opening {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--color-bg);
  pointer-events: none;
  opacity: 1;
  transition: opacity 360ms ease-out;
  box-sizing: border-box;
  padding: max(var(--space-md), env(safe-area-inset-top)) max(var(--space-md), env(safe-area-inset-right))
    max(var(--space-md), env(safe-area-inset-bottom)) max(var(--space-md), env(safe-area-inset-left));
}
.hz-opening canvas { position: absolute; display: block; background: transparent; }
.hz-opening.is-finished { opacity: 0; }
.hz-opening[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .hz-opening { transition: none; } }
@media (min-width: 1024px) { .hz-opening { padding-left: calc(var(--nav-column) + var(--space-md)); } }
.hz-view-map:has(.hz-opening:not([hidden])) #hz-map { opacity: 0; }
.hz-view-map:has(.hz-opening.is-finished) #hz-map { opacity: 1; transition: opacity 360ms ease-out; }
.hz-view-map:has(.hz-opening:not([hidden])) .hz-you,
.hz-view-map:has(.hz-opening:not([hidden])) .hz-hero,
.hz-view-map:has(.hz-opening:not([hidden])) .hz-map-controls { visibility: hidden; }
.hz-page:not([data-view="read"]):not([data-opening="complete"]) .hz-row {
  visibility: hidden;
  pointer-events: none;
  transition: none;
}
@media (prefers-reduced-motion: reduce) { .hz-view-map:has(.hz-opening.is-finished) #hz-map { transition: none; } }

/* Verified observations arrive in place while the map keeps its detail gate, at once:
   a headline faded in from transparent counts as painted only at a later repaint. */
.hz-page[data-verified-readings="true"] .hz-view-map:not([inert]):has(.hz-opening:not([hidden])) .hz-hero {
  visibility: visible;
}
.hz-wind-speed.is-pending, .hz-meta-dot.is-pending { visibility: hidden; }
.hz-view-map:not([inert]):has(.hz-opening:not([hidden])) .hz-wind-speed:not(.is-pending) {
  animation: hz-fade-in 180ms ease-out both;
}
@keyframes hz-fade-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .hz-view-map:not([inert]):has(.hz-opening:not([hidden])) .hz-wind-speed:not(.is-pending) { animation: none; }
}
