@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap");
/* ==========================================================================
   newton-liu.com

   ALL styling lives in this file (the globe landing adds globe.css on top,
   for its own g-* classes only). Every design decision that you might
   plausibly want to change is a custom property in the :root block below —
   restyling the site should mostly mean editing that block and nothing else.
   If you find yourself hunting for a hard-coded colour further down, that's a
   bug: lift it up here.

   The @import above has to stay the very first line: it is how the generated
   collection pages get the typefaces without any change to their <head>.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* ---- Grounds -------------------------------------------------------
     Night (#121212) is the default ground. Photographs carry all the colour
     on the page, so every surface here is neutral, and the one colour, the
     amber "low sun", is kept for what is active, current, focused or in
     progress. The landing page sits on --space, a shade deeper still. */
  --space:        #050505;            /* landing page and the space around the globe */
  --bg:           #121212;            /* "Night": galleries, prose, Work. The default ground */
  --bg-raised:    #1c1c1c;            /* image wells before load, inputs */
  --panel:        rgba(10,10,10,.80); /* glass: landing panel, console, card, plate (+ blur 12px) */
  --panel-solid:  #0c0c0c;            /* glass replacement: no backdrop-filter or reduced transparency */
  --glass:        color-mix(in srgb, var(--bg) 88%, transparent);  /* the masthead's frosted bar */
  --lightbox-bg:  rgba(12,12,12,.985);
  --bar:          #000000;            /* ?edit bar */

  /* The ?edit bar and its formatting bar are night on both grounds, so
     their ink is fixed rather than following the page (see .nl-bar). */
  --bar-text:         #ededea;
  --bar-text-dim:     #a8a8a5;
  --bar-line:         rgba(255,255,255,.28);
  --bar-wash:         rgba(255,255,255,.06);
  --bar-accent:       #d4a453;        /* 7.9:1 on the bar */
  --bar-accent-hover: #e0b566;
  --bar-on-accent:    #121212;

  /* ---- Lines ---------------------------------------------------------
     White at low alpha rather than greys, so a line takes the tone of
     whatever it crosses. The White ground uses black the same way. */
  --hairline:        rgba(255,255,255,.12);
  --hairline-soft:   rgba(255,255,255,.06);  /* between list rows */
  --hairline-strong: rgba(255,255,255,.28);  /* outlined buttons */
  --bracket:         rgba(255,255,255,.70);  /* the "in focus" corner brackets */
  --tile-edge:       rgba(255,255,255,.18);  /* hairline inside a hovered photograph */
  --edit-dash:       rgba(255,255,255,.30);  /* ?edit: the outline round a typeable slot */
  --wash:            rgba(255,255,255,.06);  /* hover fill on an outlined button */

  /* ---- Ink (contrast on #121212 / #050505) ---------------------------- */
  --text:       #ededea;   /* 16.0 / 17.4 */
  --text-body:  #cfcfcb;   /* prose */
  --text-dim:   #a8a8a5;   /* 7.9 / 8.6 */
  --text-faint: #8a8a87;   /* 5.4 / 5.9. The floor for any text */
  --accent:       #d4a453; /* his "low sun": active, current, focus, progress only. 8.2:1 */
  --accent-hover: #e0b566;
  --on-accent:    #121212;
  --warn:         #ffb4a2; /* ?edit: a save that failed */
  --mask-solid:   #000;    /* masks read alpha only; any opaque colour would do */

  /* ---- Type ------------------------------------------------------------
     Geist for everything read; Geist Mono, uppercase and tracked, for every
     label, date and number. Nothing on the site is set smaller than 11px. */
  --sans:  "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono:  "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --serif: var(--sans);   /* legacy name; no serif remains in English */
  /* Chinese, wherever it appears: Song characters, with the English words,
     numbers and sign-offs among them in American Typewriter, like a page off
     a typewriter. Macs have both built in. iPhones have American Typewriter
     and fetch Noto Serif SC; other devices fetch Courier Prime and Noto Serif
     SC. site.js asks Google Fonts for those only on a page with Chinese.
     "ZH Punct" (with :lang(zh), near the end) is the Song face for the
     few punctuation marks Chinese shares with English. */
  --zh:    "ZH Punct", "American Typewriter", "Courier Prime", "Songti SC", "STSong", "Noto Serif CJK SC", "Source Han Serif SC", "Noto Serif SC", serif;
  --t-zh-body: 16.5px;  /* a Chinese blurb: line-height 1.9 */
  --t-label: 11px;  /* mono, uppercase, .14–.24em, line-height 16px: the absolute minimum anywhere */
  --t-meta:  12px;  /* mono: EXIF, numbers, tabular-nums */
  --t-ui:    13px;  /* nav, buttons */
  --t-row:   14px;  /* list titles */
  --t-body:  16px;  /* line-height 1.7 */
  --t-lede:  20px;  /* line-height 1.4 */
  --t-h3:    22px;
  --t-h2:    clamp(28px, 1.2vw + 16px, 40px);
  --t-h1:    clamp(36px, 2.4vw + 16px, 60px);
  --step--1: 13px; --step-0: 16px; --step-1: 20px; --step-2: 28px; --step-3: var(--t-h1); /* legacy */

  /* ---- Space and shape ------------------------------------------------ */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px; --s9:96px;
  --edge: 16px;          /* page side margin of the window-level bars: 24px ≥621, 32px ≥1280 */
  --masthead-h: 3.5rem;  /* 4rem ≥621; the opener pull-up and the prose rail read this */
  --radius: 0;           /* everywhere, photographs included */
  --bk-len: 8px;         /* arm length of the corner brackets */
  --nl-bar-h: 2.9rem;    /* ?edit bar; the page moves down by this */
  /* What lines text in a .wrap-wide page (gallery rail, prose) up with the
     wordmark: the bars sit at --edge, the photographs at .wrap-wide's own
     padding, and this is the difference. Text only — never the photographs. */
  --rail-inset: max(0px, calc(var(--edge) - clamp(0.5rem, 2vw, 1.5rem)));

  /* ---- Layout ----------------------------------------------------------
     These are mirrored by the `sizes` hints in site.js (tileSizes,
     cappedSizes, the card's 380px), so they are not restyling knobs: change
     one and check the other.

     How tall a collection's opening photograph is allowed to be. A landscape
     opener always spans the full width of the window, and where that would
     make it taller than --opener-h it is trimmed top and bottom to fit rather
     than shrunk — so a panorama is never cut, and a 3:2 frame on a wide
     window loses a sliver. An upright is capped by height instead: centred,
     never cropped. `renderOpener()` in site.js repeats the upright number to
     size its srcset; if you change it, change that too. */
  --measure: 36rem;
  --opener-h:         100vh;
  --opener-h-upright: 88vh;
  --wrap:      74rem;   /* prose + cards */
  --wrap-wide: 108rem;  /* photo grids get more room */
  --gutter:    clamp(1.25rem, 4vw, 2.5rem);   /* legacy; .wrap now uses --edge */
  --row-h:  300px;   /* target height of a justified (legacy) gallery row */
  --gap:    10px;    /* between photographs in the legacy grid */

  /* ---- Motion ----------------------------------------------------------- */
  --ease:     cubic-bezier(.2,.7,.3,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
  --ease-io:  cubic-bezier(.65,0,.35,1);
  --d-fast: 120ms; --d-base: 240ms; --d-slow: 420ms;

  /* ---- Layers ----------------------------------------------------------- */
  --z-panel: 10; --z-card: 20; --z-masthead: 50; --z-lightbox: 100; --z-skip: 200; --z-bar: 300;
}
@media (min-width: 621px)  { :root { --edge: 24px; --masthead-h: 4rem; } }
@media (min-width: 1280px) { :root { --edge: 32px; } }
/* Legacy justified grid only; the packed gallery ignores both. */
@media (max-width: 900px)  { :root { --row-h: 210px; --gap: 8px; } }
@media (max-width: 620px)  { :root { --row-h: 148px; --gap: 5px; } }

/* "White": the second ground, switched on by the square at the end of the
   nav (site.js) and remembered per visitor. Black stays the default whatever
   the visitor's system setting. A small script in each page's <head> sets
   data-theme="light" before the first paint, so a White visit never flashes
   black. A plain paper white, so it shifts no photograph's colour; the
   amber is taken down a step so it still reads as text. Tokens not repeated
   here hold on both grounds: --glass follows --bg, and the ?edit bar keeps
   its night ink (--bar-*, --warn). A colour token added above needs its
   White value here. Contrast on #ffffff / --space / --bg-raised: */
:root[data-theme="light"] {
  color-scheme: light;
  --space:        #f4f4f2;
  --bg:           #ffffff;
  --bg-raised:    #f2f1ee;
  --panel:        rgba(255,255,255,.84);
  --panel-solid:  #ffffff;
  --lightbox-bg:  rgba(255,255,255,.985);
  --bar:          #16171b;

  --hairline:        rgba(0,0,0,.12);
  --hairline-soft:   rgba(0,0,0,.06);
  --hairline-strong: rgba(0,0,0,.28);
  --bracket:         rgba(0,0,0,.60);
  --tile-edge:       rgba(0,0,0,.18);
  --edit-dash:       rgba(0,0,0,.30);
  --wash:            rgba(0,0,0,.05);

  --text:       #16171b;   /* 17.9 / 16.3 / 15.9 */
  --text-body:  #2a2c31;   /* 14.0 */
  --text-dim:   #555860;   /* 7.1 / 6.5 / 6.3 */
  --text-faint: #64676e;   /* 5.7 / 5.1 / 5.0. The floor for any text */
  --accent:       #8f621a; /* 5.3 / 4.9 / 4.7, and white on it 5.3 */
  --accent-hover: #7f5714;
  --on-accent:    #ffffff;
}

/* A Work card's bracket colour, registered so that it can fade in. */
@property --card-bk { syntax: "<color>"; inherits: true; initial-value: transparent; }

/* The visitor's own settings, still as tokens. */
@media (prefers-contrast: more) {
  :root { --hairline: rgba(255,255,255,.30); --text-faint: #b0b0ad; }
  :root[data-theme="light"] { --hairline: rgba(0,0,0,.30); --text-faint: #4a4d54; }
}
/* Glass becomes solid when asked for, or when the browser can't blur. */
@media (prefers-reduced-transparency: reduce) {
  :root, :root[data-theme="light"] { --panel: var(--panel-solid); --glass: var(--bg); }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root, :root[data-theme="light"] { --panel: var(--panel-solid); --glass: var(--bg); }
}
/* Paper is white whatever the ground on screen. */
@media print {
  :root, :root[data-theme="light"] {
    color-scheme: light;
    --bg: #fff; --bg-raised: #fff; --text: #000; --text-body: #000;
    --text-dim: #333; --text-faint: #555; --hairline: #ccc;
  }
}

*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  transition: background-color .25s var(--ease), color .25s var(--ease);
}
/* A column, so the footer's margin-top: auto keeps it at the foot of the
   window on a short page (Contact). The landing lays itself out. */
body:not([data-page="globe"]) {
  display: flex; flex-direction: column;
  min-height: 100vh;
}

img { display: block; max-width: 100%; }
a   { color: inherit; }

::selection { background: var(--accent); color: var(--on-accent); }

/* One focus ring everywhere: amber, square, just clear of the thing. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: var(--z-skip);
  padding: 14px var(--s4);
  font: 500 var(--t-label)/16px var(--mono); letter-spacing: .14em; text-transform: uppercase;
  background: var(--accent); color: var(--on-accent); text-decoration: none;
}
.skip-link:focus { left: 0; }

/* ---------- Brackets: "in focus" --------------------------------------
   Four camera-style corner brackets, drawn as eight 1px background strips so
   they cost no extra elements. They mean one thing — this is in focus — and
   so appear only on: the globe's card and active pin, a gallery photograph
   or Work card under the pointer or keyboard, and a button under the pointer.
   Never on the header, panels or console.

   .bk draws them on the element itself; such an element must set its surface
   with background-color, never the `background` shorthand, or the brackets
   are wiped out. .bk-hover draws them on ::after and fades them in on hover
   and focus. Set --bk-c on the element (or an ancestor) to recolour them,
   --bk-len for the arm length, --bk-inset to pull them in. */
.bk, .bk-hover::after,
.grid.book .tile::after, .collection-card img,
.lb-close::after, .lb-nav::after {
  --bk-ink: var(--bk-c, var(--bracket));
  background-image:
    linear-gradient(var(--bk-ink), var(--bk-ink)), linear-gradient(var(--bk-ink), var(--bk-ink)),
    linear-gradient(var(--bk-ink), var(--bk-ink)), linear-gradient(var(--bk-ink), var(--bk-ink)),
    linear-gradient(var(--bk-ink), var(--bk-ink)), linear-gradient(var(--bk-ink), var(--bk-ink)),
    linear-gradient(var(--bk-ink), var(--bk-ink)), linear-gradient(var(--bk-ink), var(--bk-ink));
  background-repeat: no-repeat;
  background-size: var(--bk-len) 1px, 1px var(--bk-len), var(--bk-len) 1px, 1px var(--bk-len),
                   var(--bk-len) 1px, 1px var(--bk-len), var(--bk-len) 1px, 1px var(--bk-len);
  background-position: 0 0, 0 0, 100% 0, 100% 0, 0 100%, 0 100%, 100% 100%, 100% 100%;
}
.bk-hover { position: relative; }
.bk-hover::after,
.grid.book .tile::after,
.lb-close::after, .lb-nav::after {
  content: ""; position: absolute; inset: var(--bk-inset, 0); pointer-events: none;
  opacity: 0; transition: opacity var(--d-fast) var(--ease);
}
.bk-hover:hover::after, .bk-hover:focus-visible::after { opacity: 1; }

/* ---------- Containers ------------------------------------------------ */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--edge);   /* on a narrow window, flush with the masthead */
}
.wrap-wide {
  width: 100%;
  max-width: var(--wrap-wide);
  margin-inline: auto;
  padding-inline: clamp(0.5rem, 2vw, 1.5rem);
}

/* ---------- Labels, ledes, text links --------------------------------- */

.section-title {
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 var(--s5);
}

.lede {
  font: 400 var(--t-lede)/1.5 var(--sans);
  max-width: var(--measure);
  color: var(--text);
  margin: 0 0 var(--s5);
  text-wrap: pretty;
}

/* A 44px target round one line of small capitals; the negative margins hand
   the extra height back, so the line sits where a 28px link would. */
.text-link {
  display: inline-flex; align-items: center;
  min-height: 44px; margin-block: -6px -10px;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--hairline-strong);
  text-underline-offset: 7px;
  transition: text-decoration-color var(--d-fast) var(--ease);
}
.text-link:hover { text-decoration-color: var(--accent); }

/* ---------- Masthead --------------------------------------------------- */

/* A window-level bar: wordmark at the left edge, nav at the right, the same
   on every page (the landing included), so it never jumps between pages. It
   is exactly --masthead-h plus its 1px hairline; the gallery opener pulls up
   behind it by that sum, so nothing inside may make it taller. */
.masthead {
  position: sticky; top: 0; z-index: var(--z-masthead);
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--hairline);
}
.masthead .wrap { max-width: none; padding-inline: var(--edge); }

.masthead-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s5); min-height: var(--masthead-h);
}

.wordmark {
  display: grid; gap: 2px; align-content: center;
  min-height: 44px;
  text-decoration: none; color: var(--text);
}
.wordmark-name {
  font: 600 15px/1.15 var(--sans);
  letter-spacing: -.005em;
}
.wordmark-sub {
  font: 400 var(--t-label)/1.3 var(--mono);
  letter-spacing: .24em; text-transform: uppercase;
  color: var(--text-dim);
}

/* The nav follows Newton's own drawing: spaced capitals, no rule under the
   current page. The current page is marked instead by a small amber square
   before it, set outside the label so nothing shifts from page to page. */
.nav {
  display: flex; align-items: center; gap: var(--s6);
}
.nav a, .nav-tab {
  position: relative;
  display: inline-flex; align-items: center;
  padding: 15.5px 8px;                /* 13 + 31 = a 44px target, and room for the focus ring */
  margin-inline: -8px;                /* ...handed back, so the gaps stay as set */
  font: 400 var(--t-ui)/13px var(--sans);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-dim); text-decoration: none;
  white-space: nowrap;
  transition: color var(--d-fast) var(--ease);
}
/* A tab with no page of its own (Journals): a button that opens its menu,
   dressed as the links beside it, with the plain arrow, as it goes nowhere. */
.nav-tab { appearance: none; background: none; border: 0; cursor: default; }
.nav a:hover, .nav a[aria-current="page"], .nav-tab:hover, .nav-tab.is-current { color: var(--text); }
/* A 4×4 square made of borders, not a fill, so forced-colours mode keeps it. */
.nav a[aria-current="page"]::before, .nav-tab.is-current::before {
  content: "";
  position: absolute; right: 100%; top: 50%;
  margin: -2px 0 0 0;                 /* 8px of the link's own padding sits between */
  border: 2px solid var(--accent);
}

/* Black / White switch, added to the end of the nav by site.js. Its svg disc
   is replaced by a small square, half filled: the filled half moves across
   when the White ground is on (aria-pressed). A finger-sized target around
   it, without moving it. */
.theme-toggle {
  position: relative; flex: none;
  appearance: none; background: none; border: 0; cursor: pointer;
  width: 44px; height: 44px; padding: 0; margin: -15px;
  color: var(--text-dim);
  transition: color var(--d-fast) var(--ease);
}
.theme-toggle:hover { color: var(--text); }
.theme-toggle svg { display: none; }
.theme-toggle::before, .theme-toggle::after {
  content: ""; position: absolute; top: 15px; left: 15px; height: 14px;
}
.theme-toggle::before { width: 14px; border: 1.5px solid currentColor; }
.theme-toggle::after  { width: 0; border-left: 7px solid currentColor; }
.theme-toggle[aria-pressed="true"]::after { left: 22px; }

/* 中 / EN, after the ground switch at the far right of the nav (site.js):
   the language it switches to, "中" or "EN", in the switch's dim ink, on
   the same finger-sized target. "中" is marked Chinese, so it takes the
   typewriter's Song. */
.lang-toggle {
  position: relative; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  appearance: none; background: none; border: 0; cursor: pointer;
  min-width: 44px; height: 44px; padding: 0; margin: -15px;
  font: 400 var(--t-ui)/1 var(--mono); letter-spacing: .08em;
  color: var(--text-dim);
  transition: color var(--d-fast) var(--ease);
}
.lang-toggle:hover { color: var(--text); }
.lang-toggle:lang(zh) { font-size: 14px; }

/* Eight tabs (Personal, 2026-10-21) need the narrower gap below 1280px to
   fit beside the wordmark. */
@media (max-width: 1279px) {
  .nav { gap: var(--s5); }
}
@media (max-width: 1023px) {
  .nav > .lang-toggle { margin-left: -9px; }   /* boxes meet, not overlap */
}
/* On its own row (tablets), a little tighter, so the eight tabs and the two
   switches stay one line down to an iPad's 768px; narrower, the row wraps. */
@media (min-width: 621px) and (max-width: 1023px) {
  .nav { gap: 16px; }
  .nav a, .nav-tab { letter-spacing: .1em; }
}

/* Nav drop-down (NAV_MENUS in site.js): a tab that has one is wrapped in
   .nav-drop with its .nav-menu beside it. Laptops with a mouse only: on a
   phone the nav is a row that scrolls sideways and would clip it, and a
   touch screen has no pointing, so there the tab just opens its page. The
   menu hangs from the bar's bottom rule; its padding-top is a transparent
   bridge from the tab down to it, so the pointer never crosses a gap on the
   way. After the pointer leaves it stays whole for a moment, then fades, so
   a slow diagonal toward its far end still arrives; what you can see is
   what you can click throughout. Keyboard focus opens it too (focus a
   mouse click leaves behind does not hold it open), and Esc shuts it
   (.is-shut, site.js) until the pointer or focus leaves.
   The .nav-drop's z-index keeps the menu above the landing's sticky globe
   when the landing's bar is static (below 1024px wide or 620px tall). */
.nav-drop { position: relative; display: inline-flex; align-items: center; }
.nav-menu { display: none; }
/* Hung from the bar wherever the nav is one row (not a phone); shown by
   pointing (below) or, for a tab with no page, by a click or tap
   (.is-open, site.js), which is how a touch screen opens Journals. */
@media (min-width: 621px) {
  .nav-drop { z-index: var(--z-masthead); }
  .nav-menu {
    display: block;
    position: absolute; top: 100%; left: -16px;
    padding-top: calc((var(--masthead-h) - 44px) / 2 + 1px);
    pointer-events: auto;              /* the landing's bar lets clicks through */
    visibility: hidden; opacity: 0;
    transition: opacity var(--d-fast) var(--ease) 180ms, visibility 0s linear 300ms;
  }
  /* The Chinese tabs are short, so 项目 sits near the right end of the bar:
     its menu hangs from the tab's right edge, or it runs off the page. */
  :root[data-site-lang="zh"] .nav-menu { left: auto; right: -16px; }
  /* In English too, from the sixth tab on (Personal, Projects): hung from
     the left they ran past the page's edge on a laptop. */
  .nav > :nth-child(n+6) > .nav-menu { left: auto; right: -16px; }
  .nav-drop.is-open > .nav-tab { color: var(--text); }
  .nav .nav-drop.is-open .nav-menu {
    visibility: visible; opacity: 1;
    transition: opacity var(--d-fast) var(--ease), visibility 0s;
  }
}
@media (hover: hover) and (min-width: 621px) {
  .nav-drop:hover > a, .nav-drop:has(:focus-visible) > a,
  .nav-drop:hover > .nav-tab, .nav-drop:has(:focus-visible) > .nav-tab { color: var(--text); }   /* lit while its menu is open */
  .nav-drop:hover .nav-menu, .nav-drop:has(:focus-visible) .nav-menu {
    visibility: visible; opacity: 1;
    transition: opacity var(--d-fast) var(--ease), visibility 0s;
  }
  .nav .nav-drop.is-shut .nav-menu { visibility: hidden; opacity: 0; transition: none; }
}
.nav-menu-list {
  list-style: none; margin: 0; padding: var(--s2) 0;
  min-width: max-content;
  background: var(--bg);
  border: 1px solid var(--hairline); border-top: 0;
}
/* The landing's bar has no visible rule to hang from, so the box closes itself. */
body[data-page="globe"] .nav-menu-list { border-top: 1px solid var(--hairline); }
.nav .nav-menu a {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0; padding: 12px 16px;
  font: 400 var(--t-ui)/16px var(--sans);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-dim); text-decoration: none;
  white-space: nowrap;
  transition: color var(--d-fast) var(--ease);
}
.nav .nav-menu a:hover, .nav .nav-menu a:focus-visible { color: var(--text); }
.nav-menu-out { font-family: var(--mono); letter-spacing: 0; color: var(--text-faint); }

/* Five or more nav items no longer fit beside the wordmark below 1024px: the
   nav wraps to its own full-width row and the bar stops being sticky, which
   means it is no longer one --masthead-h tall, so the gallery opener stops
   pulling up behind it. Does nothing with today's four items. A tab is a
   link or a .nav-drop around one, and the switch at the end isn't one, so
   the count is the fifth child that isn't the switch. */
@media (min-width: 621px) and (max-width: 1023px) {
  .masthead:has(.nav > :nth-child(5):not(.theme-toggle):not(.lang-toggle)) { position: static; }
  .masthead-inner:has(.nav > :nth-child(5):not(.theme-toggle):not(.lang-toggle)) { flex-wrap: wrap; row-gap: 0; padding-top: var(--s1); }
  .masthead-inner:has(.nav > :nth-child(5):not(.theme-toggle):not(.lang-toggle)) .nav { flex-basis: 100%; flex-wrap: wrap; row-gap: 0; }   /* a second line when eight tabs don't fit */
  body:has(.nav > :nth-child(5):not(.theme-toggle):not(.lang-toggle))[data-page="gallery"] .book-lead { margin-top: 0; }
  body:has(.nav > :nth-child(5):not(.theme-toggle):not(.lang-toggle)) .about-band { margin-top: 0; }
}

/* Phones: two static rows, wordmark then nav. The nav row runs to the
   window's edges and scrolls sideways once it outgrows them (fading, with a
   small chevron, at an end while there is more that way, as the cue:
   .has-more and .has-less, set by site.js, which also opens the row
   scrolled to the current tab), so every page stays one tap away without a
   menu button. The
   Black/White switch is not in that row here: site.js moves it up to the
   right of the wordmark, where scrolling can't carry it out of sight. The
   opener rules below drop the pull-up at this width for the same reason. */
@media (max-width: 620px) {
  .masthead { position: static; }
  .masthead-inner {
    flex-direction: column; align-items: stretch; gap: 0;
    min-height: 0; padding-block: var(--s1) 0;
  }
  .wordmark { align-self: flex-start; }
  .nav {
    gap: 22px;
    overflow-x: auto; scrollbar-width: none;
    margin-inline: calc(-1 * var(--edge));
    padding-inline: var(--edge);
    /* A tab or menu entry scrolled into view (by focus, or a menu opening)
       stops clear of the fades below, not under them. */
    scroll-padding-inline: calc(var(--edge) + 56px);
  }
  .nav::-webkit-scrollbar { display: none; }
  /* The cue that the row goes on (.has-more at the right, .has-less at the
     left, site.js navOverflow): the tabs fade out toward that end, clear of
     the last 20px inside the page's edge, where a small chevron stands (on
     .masthead-inner, outside the mask), drawn as the journal's caret is. */
  .nav.has-more {
    -webkit-mask-image: linear-gradient(to right, var(--mask-solid) calc(100% - var(--edge) - 56px), transparent calc(100% - var(--edge) - 20px));
            mask-image: linear-gradient(to right, var(--mask-solid) calc(100% - var(--edge) - 56px), transparent calc(100% - var(--edge) - 20px));
  }
  .nav.has-less {
    -webkit-mask-image: linear-gradient(to right, transparent calc(var(--edge) + 20px), var(--mask-solid) calc(var(--edge) + 56px));
            mask-image: linear-gradient(to right, transparent calc(var(--edge) + 20px), var(--mask-solid) calc(var(--edge) + 56px));
  }
  .nav.has-less.has-more {
    -webkit-mask-image: linear-gradient(to right, transparent calc(var(--edge) + 20px), var(--mask-solid) calc(var(--edge) + 56px),
                          var(--mask-solid) calc(100% - var(--edge) - 56px), transparent calc(100% - var(--edge) - 20px));
            mask-image: linear-gradient(to right, transparent calc(var(--edge) + 20px), var(--mask-solid) calc(var(--edge) + 56px),
                          var(--mask-solid) calc(100% - var(--edge) - 56px), transparent calc(100% - var(--edge) - 20px));
  }
  .masthead-inner::before, .masthead-inner::after {
    content: ""; position: absolute; bottom: 18px;
    width: 7px; height: 7px;
    border-right: 1.25px solid var(--text-dim); border-bottom: 1.25px solid var(--text-dim);
    pointer-events: none;
    opacity: 0; transition: opacity var(--d-fast) var(--ease);
  }
  .masthead-inner::before { left: calc(var(--edge) + 2px); transform: rotate(135deg); }
  .masthead-inner::after { right: calc(var(--edge) + 2px); transform: rotate(-45deg); }
  .masthead-inner:has(> .nav.has-less)::before, .masthead-inner:has(> .nav.has-more)::after { opacity: 1; }
  /* The scrolling row would clip a ring drawn outside the link, so here it
     is drawn just inside, clear of the letters thanks to the padding. */
  .nav a:focus-visible, .nav-tab:focus-visible, .theme-toggle:focus-visible, .lang-toggle:focus-visible { outline-offset: -2px; }
  .nav a[aria-current="page"]::before, .nav-tab.is-current::before { position: static; margin: -1px 8px 0 0; }
  /* A tapped Journals opens its menu inline, in the row after it: the
     entries as tabs of their own, a short rule before them. */
  .nav-drop.is-open { gap: 22px; }
  .nav-drop.is-open > .nav-tab { color: var(--text); }
  .nav-drop.is-open .nav-menu { display: inline-flex; align-items: center; }
  .nav-drop.is-open .nav-menu::before { content: ""; width: 12px; border-top: 1px solid var(--text-faint); margin-right: 22px; }
  .nav-drop.is-open .nav-menu-list { display: flex; gap: 22px; padding: 0; border: 0; background: none; min-width: 0; }
  .nav .nav-drop.is-open .nav-menu a { padding: 15.5px 8px; margin-inline: -8px; line-height: 13px; }
  /* Centred on the wordmark row: 44px tall from the top padding, like the
     wordmark, its square flush with the right edge of the page. */
  .masthead-inner { position: relative; }
  .masthead-inner > .theme-toggle {
    position: absolute; top: calc(var(--s1) + 15px); right: var(--edge);
  }
  /* 中 / EN takes the corner, its letters flush with the page's right
     edge, and the ground switch steps in beside it. */
  .masthead-inner > .lang-toggle {
    position: absolute; top: var(--s1); right: calc(var(--edge) - 15px);
    min-width: 44px; margin: 0; padding-right: 15px; justify-content: flex-end;
  }
  .masthead-inner:has(> .lang-toggle) > .theme-toggle { right: calc(var(--edge) + 44px); }
}

/* ---------- Work index (work.html) -------------------------------------
   The collection cards from site.js's renderHome, as a contact sheet: every
   cover fills the same 3:2 frame (the camera's own), so the cards are all
   one size. A wider cover, an xPan, is cropped to its centre here only;
   galleries show it whole. */

.collections { padding-block: 0 var(--s8); }

.work-head {
  display: flex; align-items: baseline; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s2) var(--s5);
  padding-top: var(--s8);
  margin-bottom: var(--s4);
}
.work-head .section-title {
  margin: 0;
  font: 500 var(--t-h2)/1.1 var(--sans);
  letter-spacing: -.015em;
  text-transform: none;
  color: var(--text);
}

/* Globe · Index: the current view takes the same amber square as the nav. */
.view-switch {
  display: flex; align-items: center; gap: var(--s6);
  margin: 0;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em; text-transform: uppercase;
}
.view-switch a, .view-switch [aria-current] {
  display: inline-flex; align-items: center; min-height: 44px;
}
.view-switch a {
  min-width: 44px;                    /* "Globe" alone is narrower than a finger */
  color: var(--text-faint); text-decoration: none;
  transition: color var(--d-fast) var(--ease);
}
.view-switch a:hover { color: var(--text); }
.view-switch [aria-current] { color: var(--text); }
.view-switch [aria-current]::before {
  content: ""; margin: -1px 8px 0 0; border: 2px solid var(--accent);
}

.work-lede {
  font: 400 var(--t-body)/1.6 var(--sans);
  color: var(--text-dim);
  margin: 0 0 40px;
}

/* About 352px a card at full width (three across), which is what the
   card's 380px `sizes` hint in site.js assumes. */
.collection-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: 40px 24px;
}

.collection-card {
  position: relative;
  display: block;
  text-decoration: none;
  color: var(--text);
}
.collection-card:focus-visible { outline-offset: 10px; }

/* The brackets are drawn in a 6px ring of padding round the photograph,
   which the negative margins hand back so the photograph still lines up
   with the title. */
.collection-card img {
  display: block;
  box-sizing: content-box;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: 50% 50%;
  padding: 6px; margin: -6px;
  background-color: transparent;
  --bk-len: 14px;
  --bk-c: var(--card-bk);              /* transparent until hovered (see @property) */
  transition: opacity .5s var(--ease), --card-bk var(--d-fast) var(--ease);
}
.collection-card:hover img,
.collection-card:focus-visible img { --card-bk: var(--bracket); }

.card-body { padding: var(--s3) 0 0; }
.card-title {
  margin: 0;
  font: 500 18px/1.3 var(--sans);
  letter-spacing: -.005em;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--d-fast) var(--ease);
}
.collection-card:hover .card-title,
.collection-card:focus-visible .card-title { text-decoration-color: currentColor; }
.card-sub {
  margin: var(--s1) 0 0;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-faint);
}
/* An empty collection shouldn't look broken — it looks *pending*. (None is
   listed today; the rule is a backstop.) */
.collection-card.is-empty::before {
  content: ""; display: block; aspect-ratio: 3 / 2;
  border: 1px dashed var(--hairline-strong);
}
.collection-card.is-empty .card-title { color: var(--text-dim); }

/* ---------- Gallery page: the rail ------------------------------------ */

.back-link, .book-onward a {
  display: block; width: fit-content;
  padding-block: 14px; margin-block: -14px;   /* a 44px target that takes one line */
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-decoration: none;
  transition: color var(--d-fast) var(--ease);
}
.back-link:hover, .book-onward a:hover { color: var(--text); }

/* The way onward, under the last photographs (site.js onward()): the
   collections either side of this one in the Index's order, in the back
   link's small capitals, the newer at the left and the older at the right,
   under the photographs' column, its text inset like the rail's (so on a
   laptop the right one ends under the footer's links, and on a phone both
   line up with the wordmark); on a phone a long pair takes two lines. */
.book-onward {
  grid-column: 2;
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s4) var(--s5);
  padding: var(--s6) var(--rail-inset) var(--s7);
}
.book-onward .book-onward-next { margin-left: auto; text-align: right; }

.gallery-title {
  font: 500 var(--t-h1)/1.05 var(--sans);
  letter-spacing: -.02em;
  margin: var(--s4) 0 var(--s3);
  text-wrap: balance;
}
/* One date for the whole collection, sitting under its title. */
.gallery-date {
  margin: 0 0 var(--s2);
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.gallery-sub {
  margin: 0 0 var(--s2);
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
}
/* A blurb can run to several paragraphs: collections.json separates them with
   a blank line ("\n\n"), and pre-line shows that line as the gap. */
.gallery-blurb {
  max-width: 34rem;
  margin: var(--s5) 0 0;
  font: 400 var(--t-body)/1.7 var(--sans);
  color: var(--text-dim);
  white-space: pre-line;
}
/* A blurb in Chinese takes the typewriter pairing from :lang(zh) (at the end
   of this file), a touch larger and with more air between lines. It's ragged
   right because a typewriter can't justify, so every character keeps the same
   pitch. Safari hangs a closing 。or ，past the edge rather than wrapping it. */
.gallery-blurb:lang(zh) {
  font-size: var(--t-zh-body);
  line-height: 1.9;
  color: var(--text-body);
  text-align: start;
  hanging-punctuation: allow-end;
}

/* All optional — collapse rather than leaving a gap. */
.gallery-date:empty, .gallery-sub:empty, .gallery-blurb:empty { display: none; }

/* Justified rows via flexbox.
   Each tile carries its aspect ratio in --ar, set inline from photos.json:
     flex-basis = ar * row-h  → its natural width at the target row height
     flex-grow  = ar          → every tile in a row scales by the same factor,
                                so their heights stay equal
     aspect-ratio = ar        → height follows from the final width
   The ::after soaks up leftover grow so a lone last photo isn't stretched.
   The packed gallery (.grid.book, below) overrides most of this, but the
   base .tile still gives it its box rules, so treat it as live. */

.grid { display: flex; flex-wrap: wrap; gap: var(--gap); }
.grid::after { content: ""; flex-grow: 1e6; }

.grid-status {
  width: 100%; text-align: center; padding: 4rem 1rem;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-faint);
}

.tile {
  --ar: 1.5;
  position: relative;
  flex-grow: var(--ar);
  flex-shrink: 1;
  flex-basis: calc(var(--ar) * var(--row-h));
  aspect-ratio: var(--ar);
  max-width: 100%;
  margin: 0; padding: 0; border: 0;
  background: var(--bg-raised);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: zoom-in;
}

/* No zoom on hover: the photograph holds still and the brackets come up. */
.tile img {
  width: 100%; height: 100%; object-fit: cover;
  transition: opacity 0.5s ease;
}
.tile img[data-loaded="false"] { opacity: 0; }

/* The hover caption behind CAPTIONS.grid in site.js. Both switches are off,
   so nothing is ever printed on a photograph; this stays so that flipping one
   still works as CLAUDE.md describes. */
.tile-label {
  position: absolute; inset: auto 0 0 0;
  padding: var(--s7) var(--s4) var(--s3);
  background: linear-gradient(to top, var(--panel-solid), transparent);
  color: var(--text); text-align: left;
  opacity: 0; transition: opacity var(--d-base) var(--ease);
  pointer-events: none;
}
.tile:hover .tile-label, .tile:focus-visible .tile-label { opacity: 1; }
.tile-title { display: block; font: 500 var(--t-row)/1.3 var(--sans); }
.tile-place {
  display: block; margin-top: 2px; color: var(--text-dim);
  font: 400 var(--t-label)/16px var(--mono); letter-spacing: .14em; text-transform: uppercase;
}

/* ---------- Prose pages (legacy) ------------------------------------------
   Nothing uses .prose now; it is the fallback if a plain text page is ever
   wanted. .contact-list is live, inside Contact's prose. */

.prose { padding-block: clamp(3rem, 8vw, 5.5rem); }
.prose h1 {
  font: 500 var(--t-h1)/1.05 var(--sans);
  margin: 0 0 1.5rem; letter-spacing: -.02em;
}
.prose h2 {
  font: 500 var(--t-h3)/1.25 var(--sans);
  margin: 2.5rem 0 0.75rem;
}
.prose p { max-width: var(--measure); color: var(--text-body); margin: 0 0 1.15rem; }
.prose a { color: var(--text); text-decoration-color: var(--hairline-strong); text-underline-offset: 4px; }

/* Rows a full finger tall, with a hairline between each. */
.contact-list {
  list-style: none; margin: var(--s5) 0 0; padding: 0;
}
.contact-list li {
  display: flex; align-items: center; column-gap: .3em;   /* "Instagram:" and its linked handle */
  min-height: 48px;
}
.contact-list li + li { border-top: 1px solid var(--hairline); }
.contact-list a {
  display: inline-flex; align-items: center;
  min-height: 44px; min-width: 44px;  /* the row's height, not just the line's */
}

/* ---------- Footer -------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--hairline);
  padding-block: var(--s6) var(--s7);
  margin-top: auto;
}
.footer .wrap { max-width: none; padding-inline: var(--edge); }   /* aligned with the masthead */
.footer-inner {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  align-items: center; justify-content: space-between;
}
.footer p { margin: 0; color: var(--text-faint); font: 400 var(--t-ui)/1.5 var(--sans); }
.footer .footer-links { display: flex; gap: var(--s5); }
.footer-links a {
  display: inline-flex; align-items: center; min-height: 44px; min-width: 44px;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-faint); text-decoration: none;
  transition: color var(--d-fast) var(--ease);
}
.footer-links a:hover { color: var(--text); }

/* ---------- Lightbox ------------------------------------------------------ */

.lightbox {
  position: fixed; inset: 0; z-index: var(--z-lightbox);
  background: var(--lightbox-bg);
  display: grid; place-items: center;
  /* The margin round the photograph, and how far a 48px arrow at the
     window's edge would reach past it onto the photograph (none from
     1200px up, where the margin is a full 3rem). */
  --lb-pad: clamp(1rem, 4vw, 3rem);
  --lb-over: max(0px, 48px - var(--lb-pad));
  padding: var(--lb-pad);
  animation: fade-in var(--d-base) var(--ease);
}
.lightbox[hidden] { display: none; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.lb-figure {
  margin: 0; display: flex; flex-direction: column; align-items: center;
  gap: 1.1rem; max-width: 100%; max-height: 100%;
}
.lb-image {
  max-width: 100%;
  max-height: calc(100vh - 13rem);   /* leaves room for the caption */
  object-fit: contain;
}
.lb-caption { text-align: center; max-width: 46rem; }
.lb-title {
  font: 500 var(--t-lede)/1.3 var(--sans);
  margin: 0 0 0.25rem;
}
.lb-meta {
  margin: 0.15rem 0 0; color: var(--text-dim);
  font: 400 var(--t-meta)/1.5 var(--mono); letter-spacing: .02em;
}
/* The camera line, a blank line, then the animal's name where one is given
   and the date (wireLightbox moves .lb-meta last; Newton, 2026-10-19). */
.lb-exif {
  margin: 0 0 1.5em; color: var(--text-faint);
  font: 400 var(--t-meta)/1.5 var(--mono);
  letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
}
.lb-name {
  margin: 0.15rem 0 0; color: var(--text-dim);
  font: 400 var(--t-meta)/1.5 var(--mono);
  letter-spacing: .02em;
}
.lb-name a { color: inherit; text-decoration: none; }
.lb-name a:hover, .lb-name a:focus-visible { color: var(--text); text-decoration: underline; text-underline-offset: .2em; }
.lb-row { display: block; }          /* one per animal in the frame */
/* On a touch screen each animal's link is a 24px target, clear of the next. */
@media (pointer: coarse) {
  .lb-name a { padding-block: 4px; }
  .lb-row + .lb-row { margin-top: 8px; }
}
.lb-sci { margin-left: .7em; font-style: italic; }
.lb-out { margin-left: .4em; color: var(--text-faint); }
.lb-title:empty, .lb-meta:empty, .lb-exif:empty, .lb-name:empty { display: none; }

/* No caption at all — give the photograph the room back. */
.lightbox.is-bare .lb-figure { gap: 0; }
.lightbox.is-bare .lb-image { max-height: calc(100vh - 5rem); }

/* Square controls on the ground: close outlined, the arrows bare until the
   pointer finds them. All three bring up the brackets on hover. */
.lb-close, .lb-nav {
  position: absolute;
  display: grid; place-items: center;
  padding: 0;
  appearance: none; background: none; border: 0; border-radius: 0;
  color: var(--text-dim); cursor: pointer;
  font-family: var(--sans); font-weight: 300; line-height: 1;
  transition: color var(--d-fast) var(--ease), background-color var(--d-fast) var(--ease);
}
.lb-close:hover, .lb-nav:hover { color: var(--text); }
.lb-close:hover::after, .lb-nav:hover::after { opacity: 1; }
/* On hover the close button's square draws back to its corners. */
.lb-close {
  top: var(--s4); right: var(--s4);
  width: 44px; height: 44px;
  border: 1px solid var(--hairline-strong);
  font-size: 26px;
  padding-bottom: 2px;               /* optical: × sits low in the em */
  --bk-inset: -1px;
}
.lb-close:hover { border-color: transparent; }
.lb-nav {
  top: 50%; transform: translateY(-50%);
  width: 48px; height: 48px;
  border: 1px solid transparent;     /* invisible, except in forced colours */
  font-size: 40px;
  padding-bottom: 5px;               /* optical: the chevrons sit low in the em */
}
/* Flush with the window's edges, so the arrows stand in the lightbox's side
   margin. Where that margin is narrower than the arrow, the glyph and its
   brackets keep to the margin and only the invisible rest of the target
   reaches over the photograph: nothing is drawn on it. */
.lb-prev  { left: 0;  padding-right: var(--lb-over); --bk-inset: 0 var(--lb-over) 0 0; }
.lb-next  { right: 0; padding-left:  var(--lb-over); --bk-inset: 0 0 0 var(--lb-over); }

/* One photograph alone (a journal entry's): nothing to step to. */
.lightbox.is-single .lb-nav { display: none; }
.lb-nav:focus-visible { outline-offset: -2px; }   /* at the window's edge: drawn inside */

body.lb-open { overflow: hidden; }

/* ---------- Responsive ---------------------------------------------------- */

/* On a phone the arrows move to the bottom corners, under the thumb. So they
   do on any window narrower than 6:5 below 1200px: there the side margin
   (4vw) is thinner than an arrow and a 3:2 photograph fills the width, but
   the caption's allowance leaves room underneath. */
@media (max-width: 620px), (max-width: 1199px) and (max-aspect-ratio: 6 / 5) {
  .lb-nav { top: auto; bottom: var(--s4); transform: none; padding-inline: 0; --bk-inset: 0; }
  .lb-prev { left: var(--s4); }
  .lb-next { right: var(--s4); }
}

@media (max-width: 620px) {
  .lb-image { max-height: calc(100vh - 15rem); }

  .footer-inner { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

@media print {
  .masthead, .lightbox, .footer { display: none !important; }
  body { background: var(--bg); color: var(--text); }
}
/* ==========================================================================
   Packed-grid gallery.

   Rows are flush at every window width because exactly one cell per row
   carries an aspect-ratio (the anchor) and every other cell stretches to the
   height that cell sets. No measurement, no resize handling.

   Every rule in this section that sets a size, a share or a breakpoint is
   mirrored by a `sizes` hint in site.js; the skin (colours, brackets) is
   the only part that is free to change.
   ========================================================================== */

/* --- opener: the full width of the window ------------------------------ */

/* Full bleed. The element carries .wrap-wide, which would centre it inside
   108rem and hold it off the window edges; both are overridden here, and this
   rule wins on source order rather than specificity, so it has to stay below
   .wrap-wide in the file. */
.book-lead {
  display: flex;
  justify-content: center;
  max-width: none;
  padding: 0 0 clamp(26px, 4.5vw, 72px);
}

/* ...and up behind the masthead, so the photograph starts at the very top of
   the window with no band above it. The bar is sticky and therefore in
   flow, so the opener is pulled back by exactly its height plus its hairline.
   Below 620px the bar goes static and stacks into two rows, so its height is
   no longer --masthead-h and the bleed is dropped rather than guessed at. */
@media (min-width: 621px) {
  body[data-page="gallery"] .book-lead {
    margin-top: calc(-1 * (var(--masthead-h) + 1px));
  }

  /* Over a photograph the bar is the same frosted glass as everywhere else,
     88% ground, with its hairline made transparent so no rule crosses the
     opener. On Black the worst case is a white sky under the bar; measured
     there, the dim nav still holds 5.7:1. On White it is a black night sky,
     and the dim nav holds 5.4:1. */
  body[data-page="gallery"] .masthead {
    border-bottom-color: transparent;
  }
}

.book-opener {
  margin: 0;
  cursor: zoom-in;
  display: flex;
  justify-content: center;
  width: 100%;
}
/* Full bleed, so the ring is drawn inside it rather than off the window. */
.book-opener:focus-visible { outline-offset: -6px; }

/* A landscape opener is always the width of the window. Where that would
   make it taller than --opener-h, the box stops there and object-fit trims
   the photograph top and bottom — evenly, or around its `focus` from
   photos.json. A panorama never reaches the cap, so it is never cut. */
.book-opener img {
  display: block;
  width: 100%;
  height: auto;
  max-height: var(--opener-h);
  object-fit: cover;
}

/* An upright spanning the window would be several screens tall, and a square
   would lose a third of itself to the trim, so both keep their own shape
   instead: centred, capped by height, uncropped. */
.book-opener.is-upright img {
  width: auto;
  max-width: 100%;
  max-height: var(--opener-h-upright);
}

/* Some landscapes can't be trimmed to one screen: a standing bird that fills
   its frame from crown to bill would lose one end, and whatever was left at
   the top would sit under the bar. `"opener": "whole"` in collections.json
   shows that photograph entire instead: still the width of the window, but
   at its own height, starting below the bar, and running past the fold. */
.book-lead.is-whole .book-opener:not(.is-upright) img { max-height: none; }

@media (min-width: 621px) {
  body[data-page="gallery"] .book-lead.is-whole { margin-top: 0; }
}

/* --- body: text column left, photographs right ------------------------ */

.book-body {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) 62%;
  column-gap: 3%;
  align-items: start;
}

.book-rail { text-align: left; padding-inline: var(--rail-inset); }

@media (max-width: 1000px) {
  .book-body { display: block; }
  .book-rail { margin-bottom: 4vh; }
}

/* --- blocks ------------------------------------------------------------ */

.grid.book {
  --book-gap: 10px;
  display: block;
  min-width: 0;
}

.book-row {
  display: flex;
  gap: var(--book-gap);
  align-items: stretch;
  margin-bottom: var(--book-gap);
}

.book-row.is-solo { justify-content: center; }

.book-col {
  display: flex;
  flex-direction: column;
  gap: var(--book-gap);
  min-width: 0;
}

/* Cells crop to fill, so a landscape can become a squarer landscape and an
   upright a squarer upright — never one into the other. Each cell is a
   --bg-raised well until its photograph has loaded. */
.grid.book .tile {
  position: relative;
  display: block;
  width: 100%;
  flex: 1 1 0;
  min-height: 0;
  padding: 0;
  border: 0;
  margin: 0;
  background: var(--bg-raised);
  overflow: hidden;
  cursor: zoom-in;
  aspect-ratio: auto;
}

.grid.book .tile.is-anchor {
  flex: 0 0 auto;
  aspect-ratio: var(--cell-ar);
}

.grid.book .tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* A photograph that packs with nothing else keeps its own ratio. */
.grid.book .tile.is-natural {
  flex: 0 0 auto;
  aspect-ratio: var(--cell-ar);
}

/* Under the pointer or keyboard a photograph is "in focus": a hairline
   inside its edge and the corner brackets 8px in. Nothing is ever printed
   on it. Focus adds the amber ring outside, like everywhere else. */
.grid.book .tile { --bk-len: 14px; --bk-inset: 8px; }
.grid.book .tile:hover { outline: 1px solid var(--tile-edge); outline-offset: -1px; }
.grid.book .tile:hover::after,
.grid.book .tile:focus-visible::after { opacity: 1; }
.grid.book .tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The bar is sticky from 621px: a photograph or card that focus or a skip
   link scrolls to stops clear of it rather than under it. */
@media (min-width: 621px) {
  #grid, #collection-grid, .grid.book .tile, .collection-card { scroll-margin-top: calc(var(--masthead-h) + 1rem); }
}

/* A four-frame sequence (SSSS in site.js). Four across on a phone would make
   each frame a thumbnail, so below the breakpoint the strip folds into two
   rows of two, which still reads in order. The !important beats the flex
   share site.js sets inline on each column. */
@media (max-width: 620px) {
  .book-row.is-seq { flex-wrap: wrap; }
  .book-row.is-seq > .book-col { flex: 0 0 calc((100% - var(--book-gap)) / 2) !important; }
}

/* A collection's featured upright (PLLL in site.js). At 64% of a phone the
   upright would be a thumbnail, so below the breakpoint the row folds into a
   column: the upright full width at its own ratio, then the three landscapes,
   each given a 3:2 box since they no longer have its height to stretch to.
   The !important again beats the inline flex share, whose zero basis would
   collapse each column to nothing once they stack. */
@media (max-width: 620px) {
  .book-row.is-feature { flex-direction: column; }
  .book-row.is-feature > .book-col { flex: 0 0 auto !important; }
  .book-row.is-feature .tile:not(.is-natural) { flex: 0 0 auto; aspect-ratio: 3 / 2; }
}

/* ==========================================================================
   Prose pages (About, Contact)

   The gallery's vocabulary applied to text: a left rail against a right-hand
   column, small tracked mono labels, hairline rules between sections. About
   opens on a photograph the way a gallery does, full bleed (.about-band).
   ========================================================================== */

/* About's cover is full bleed, as a gallery opens (Newton asked,
   2026-10-15): the width of the window and up behind the bar, shown whole;
   a panorama never reaches the --opener-h cap. The element carries
   .wrap-wide, overridden here on source order, as .book-lead is. The sizes
   hint in about.html says 100vw to match. */
.about-band {
  max-width: none;
  padding: 0 0 clamp(2.5rem, 8vh, 6rem);
}

.about-band img {
  display: block;
  width: 100%;
  height: auto;
  max-height: var(--opener-h);
  object-fit: cover;
}

.about-band-open {
  display: block; width: 100%; margin: 0; padding: 0;
  border: 0; border-radius: 0; background: none; cursor: zoom-in;
}
.about-band-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -6px; }

@media (min-width: 621px) {
  .about-band { margin-top: calc(-1 * (var(--masthead-h) + 1px)); }
  body:has(.about-band) .masthead { border-bottom-color: transparent; }
}

.about-body {
  padding-inline: max(var(--edge), clamp(0.5rem, 2vw, 1.5rem));   /* text aligned with the masthead */
  display: grid;
  grid-template-columns: minmax(200px, 1fr) 62%;
  column-gap: 3%;
  align-items: start;
  padding-bottom: clamp(4rem, 12vh, 8rem);
}

/* No band above it, so it has to make its own headroom. */
.about-body.is-bare { padding-top: clamp(3rem, 8vw, 5.5rem); }

.about-rail {
  position: sticky;
  top: calc(var(--masthead-h) + 2rem);
  text-align: left;
}

.about-title {
  margin: 0;
  font: 500 var(--t-h2)/1.1 var(--sans);
  letter-spacing: -.015em;
  color: var(--text);
}

.about-name {
  margin: var(--s3) 0 0;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.about-jump {
  display: flex;
  flex-direction: column;
  max-width: 15rem;
  margin-top: var(--s6);
  border-top: 1px solid var(--hairline-soft);
}

.about-jump a {
  display: flex; align-items: center;
  min-height: 32px;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-decoration: none;
  border-bottom: 1px solid var(--hairline-soft);
  transition: color var(--d-fast) var(--ease);
}
.about-jump a:hover { color: var(--text); }

.about-prose {
  min-width: 0;
  max-width: 62ch;
  font: 400 var(--t-body)/1.7 var(--sans);
  color: var(--text-body);
}

/* The opening paragraph, whichever one it is — a position rather than a
   class, so that rewriting the page in edit mode can't lose the lede by
   deleting the paragraph that happened to be carrying it. */
.about-prose > p:first-child {
  margin: 0 0 var(--s7);
  font-size: var(--t-lede);
  line-height: 1.4;
  letter-spacing: -.005em;
  color: var(--text);
  text-wrap: pretty;
}

.about-prose p {
  margin: 0 0 1.5em;
  text-wrap: pretty;
}

/* About written in Chinese takes a Chinese blurb's size and air (see
   .gallery-blurb:lang(zh)), ragged right, one size throughout: its opening
   paragraph is set like the rest (Newton asked), not as a lede. */
.about-prose p:lang(zh) {
  font-size: var(--t-zh-body);
  line-height: 1.9;
  text-align: start;
  hanging-punctuation: allow-end;
}
.about-prose > p:first-child:lang(zh) { margin-bottom: 1.5em; color: var(--text-body); }
/* An English line straight after Chinese (the essay's sign-off) takes the
   same typewriter and size, as it does at the end of a Chinese blurb; its
   lang stays English. So does one after a figure, which only ever follows
   a Chinese paragraph (figure 9 sits above the sign-off). An English
   section after a heading keeps the sans. */
.about-prose :is(p:lang(zh), .about-fig) + p:not(:lang(zh)) {
  font: 400 var(--t-zh-body)/1.9 var(--zh);
  letter-spacing: .05em;
  -webkit-font-smoothing: auto;
}

/* Section headings are ruled mono labels, not display type — the rail
   already carries the page's voice. */
.about-prose h2 {
  margin: var(--s7) 0 var(--s4);
  padding-top: var(--s5);
  border-top: 1px solid var(--hairline);
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-faint);
  scroll-margin-top: calc(var(--masthead-h) + 2rem);
}
.about-prose h3 {
  margin: var(--s6) 0 var(--s2);
  font: 500 18px/1.35 var(--sans);
  color: var(--text);
}
.about-prose ul:not(.contact-list), .about-prose ol { margin: 0 0 1.5em; padding-left: 1.2em; }
.about-prose blockquote {
  margin: 0 0 1.5em; padding-left: var(--s4);
  border-left: 1px solid var(--hairline-strong);
  color: var(--text-dim);
}

.about-prose > *:first-child { margin-top: 0; }

.about-prose a, .contact-list a, .gallery-blurb a {
  color: var(--text);
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--hairline-strong);
  text-underline-offset: 4px;
  transition: text-decoration-color var(--d-fast) var(--ease);
}
.about-prose a:hover, .contact-list a:hover, .gallery-blurb a:hover { text-decoration-color: var(--accent); }

/* Reuses .contact-list from the old prose rules; the rhythm here is set by
   the paragraph above it. */
.about-prose .contact-list { margin: 0 0 var(--s4); }

/* About in Chinese or English (Newton asked, 2026-10-15). Each version is
   everything marked data-lang; Chinese shows unless html[data-about-lang="en"]
   (site.js aboutLanguage: the site in English and an English story written).
   Only About carries data-lang, so these touch no other page. */
:root[data-about-lang="en"] [data-lang="zh"],
:root:not([data-about-lang="en"]) [data-lang="en"] { display: none; }

/* The page opens on My story: its title is for screen readers only. */
.about-title[data-lang] {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Characters at the labels' 11px are too small to read: About's Chinese
   labels (the rail, the section heads) are a size up. */
.about-jump[data-lang] a:lang(zh),
.about-prose[data-lang] h2:lang(zh) { font-size: 13px; }

/* The Chinese gear list in the essay's size, like its paragraphs. */
.about-prose[data-lang] li:lang(zh) { font-size: var(--t-zh-body); line-height: 1.9; }
/* And the Chinese version's contact list in its typewriter, every row inked
   alike (邮箱 is marked Chinese, the others aren't); a full-width ： brings
   its own space. */
.about-prose[data-lang="zh"] .contact-list { font: 400 var(--t-zh-body)/1.9 var(--zh); letter-spacing: .05em; -webkit-font-smoothing: auto; }
.about-prose[data-lang="zh"] .contact-list li { column-gap: 0; }

/* The essay's figures (site.js aboutFigures), Newton's photographs for its
   "（图N）" marks, each mark a link to its figure. Every photograph but
   the two charts (图1, 图2) and the upright Arches (图8) is shown at the
   ptarmigan's 3:2 (图7; Newton asked, 2026-10-15), cropped here and
   never in the file: data-crop on the figure, the crop's centre in its
   img's object-position. A photograph clicked opens full screen, whole.
   The box takes its shape before the photograph loads (lazy), from the img's
   width and height or the crop, or a jump to 设备 would land short. */
.about-body.has-figures { container-type: inline-size; }
.about-fig { margin: var(--s6) 0 var(--s7); }
.about-fig img { display: block; width: 100%; height: auto; cursor: zoom-in; }
.about-fig[data-crop] img { aspect-ratio: 3 / 2; object-fit: cover; }
.about-fig figcaption {
  margin-top: var(--s3);
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color var(--d-base) var(--ease);
}
/* Newton's captions, "图1：…" : Chinese, so the typewriter from :lang(zh),
   at the rail's Chinese label size; whole sentences, so open enough to
   read over two or three lines. */
.about-fig figcaption:lang(zh) { font-size: 13px; line-height: 1.7; letter-spacing: .06em; text-transform: none; }

.about-prose a.fig-mark {
  color: inherit;
  text-decoration: underline dotted;
  text-decoration-color: var(--text-faint);
  text-underline-offset: .3em;
  transition: color var(--d-base) var(--ease);
}
.about-prose a.fig-mark:hover,
.about-prose a.fig-mark.is-current { color: var(--accent); text-decoration-color: currentColor; }

.fig-zoom {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: var(--bg);
  cursor: zoom-out;
}
.fig-zoom:focus { outline: none; }
.fig-zoom img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

/* 图2, the eBird map (site.js birdMap). A 3:2 box like the photographs
   beside it: a line of labels at the top, the map, the scale at the foot,
   in the ground's own colours, so it is dark on the dark ground and light
   on the light one (Newton, 2026-10-16); the shading is eBird's. */
.bird-map {
  --bm-land: color-mix(in srgb, var(--text) 11%, transparent);
  --bm-edge: var(--bg);
  --bm-sphere: color-mix(in srgb, var(--text) 9%, transparent);
  --bm-ink: var(--text);
  --bm-faint: var(--text-faint);
  --bm-tip-bg: var(--bg);
  --bm-tip-edge: color-mix(in srgb, var(--text) 18%, transparent);
  --bm-hot: var(--text);
  position: relative;
  aspect-ratio: 3 / 2;
  width: 100%;
  overflow: hidden;
  color: var(--bm-ink);
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
.bm-head, .bm-legend {
  position: absolute; left: 0; right: 0;
  display: flex; align-items: center; gap: .6em;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--bm-faint);
}
.bm-head { top: 0; }
.bm-legend { bottom: 0; }
.bm-legend i { display: block; width: 7.5em; height: 6px; border-radius: 3px; }
.bm-crumb {
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; background: none; border: 0; padding: 0; margin: 0;
}
.bm-crumb.is-link { color: var(--bm-ink); cursor: pointer; text-decoration: underline dotted; text-underline-offset: .3em; }
.bm-crumb.is-link:hover { color: var(--accent); }
.bm-crumb:disabled { cursor: default; }
.bm-here:not(:empty)::before { content: "›"; margin-right: .6em; color: var(--bm-faint); }
.bm-here { color: var(--bm-ink); }
.bm-total { margin-left: auto; }

.bm-views { position: absolute; left: 0; right: 0; top: 26px; bottom: 22px; }
.bm-view {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; visibility: hidden;
  transition: opacity .34s var(--ease), visibility 0s .34s;
  overflow: visible;
}
.bm-view.is-shown { opacity: 1; visibility: visible; transition: opacity .34s var(--ease); }
.bm-outline { fill: none; stroke: var(--bm-sphere); stroke-width: 1; vector-effect: non-scaling-stroke; }
.bm-rest { fill: var(--bm-land); stroke: var(--bm-edge); stroke-width: .6; vector-effect: non-scaling-stroke; }
.bm-shape, .bm-dot { stroke: var(--bm-edge); stroke-width: .6; vector-effect: non-scaling-stroke; }
.bm-shape.is-hot, .bm-dot.is-hot { stroke: var(--bm-hot); stroke-width: 1.4; }
.bird-map.is-moving .bm-shape { stroke-width: .4; }

.bm-tip {
  position: absolute; left: 0; top: 0;
  display: grid; gap: 2px;
  padding: 8px 11px 9px;
  background: var(--bm-tip-bg);
  border: 1px solid var(--bm-tip-edge);
  border-radius: 4px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
  font: 400 13px/1.35 var(--sans);
  color: var(--bm-ink);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s;
  z-index: 2;
}
.bm-tip.is-on { opacity: 1; }
.bm-tip b { font-weight: 600; }
.bm-tip em:empty { display: none; }
.bm-tip em { font: 400 var(--t-label)/16px var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--bm-faint); font-style: normal; }

/* In the stage the figures let the pointer through; this one takes it. */
.about-stage .about-fig.is-in .bird-map { pointer-events: auto; }

@media (min-width: 1001px) {
  /* Newton's layout (2026-10-15): the text against the right edge, the
     left column the rail and, under it, a stage for the figures, each its
     full width, two at a time when both fit (site.js places them, --y). A
     figure moves up as the reading passes its mark: in at the bottom, out
     at the top. */
  .about-body.has-figures { grid-template-columns: minmax(0, 1fr) auto; column-gap: clamp(2rem, 4vw, 4.5rem); }
  .about-body.has-figures > .about-rail { grid-column: 1; grid-row: 1; }
  .about-body.has-figures > .about-prose { grid-column: 2; grid-row: 1; width: min(62ch, 52cqi); }
  /* The page's closing space is the text column's own, not .about-body's,
     so the rail (which sticks only as far as the grid reaches) keeps that
     much more room at the foot of the page for the Gear photograph. */
  .about-body.has-figures { padding-bottom: 0; }
  .about-body.has-figures > .about-prose { padding-bottom: clamp(4rem, 12vh, 8rem); }

  /* The rail fills the window's height and holds the stage over all of
     it. With 图1 and the Gear photograph its contents stay up; while any
     other figure is shown they fold away (.is-folded) and the figure has
     the whole column (Newton asked, 2026-10-16). Figures moving out at the
     top are clipped at the column's edge. */
  .about-rail.has-stage { height: calc(100svh - var(--masthead-h) - 4rem); }
  .about-rail.has-stage > :not(.about-stage):not(.about-beside) {
    transition: opacity var(--d-slow) var(--ease), transform var(--d-slow) var(--ease), visibility 0s;
  }
  .about-rail.has-stage.is-folded:not(:focus-within) > :not(.about-stage):not(.about-beside) {
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity var(--d-slow) var(--ease), transform var(--d-slow) var(--ease), visibility 0s var(--d-slow);
  }
  .about-stage {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
  }
  .about-stage .about-fig {
    position: absolute; top: 0; left: 0;
    margin: 0;
    opacity: 0;
    transform: translateY(var(--y, 100vh));
    transition: transform .7s var(--ease), opacity .5s var(--ease);
  }
  .about-stage .about-fig.is-in { opacity: 1; pointer-events: auto; }
  .about-stage .about-fig.is-current figcaption { color: var(--text); }
  /* 图1 sits beside the contents, between them and the text (Newton
     asked): from the rail's top, right of the contents (site.js sets left).
     The Gear photograph sits in the stage under the contents. */
  .about-beside { position: absolute; top: 0; right: 0; bottom: 0; pointer-events: none; }
  .about-beside .about-fig {
    position: absolute; top: 0; left: 0;
    margin: 0;
    opacity: 0;
    transform: translateY(var(--y, 24px));
    transition: transform .7s var(--ease), opacity .5s var(--ease);
  }
  .about-beside .about-fig.is-in { opacity: 1; pointer-events: auto; }
}

@media (max-width: 1000px) {
  .about-body { display: block; }
  .about-fig { width: 100cqi; }
  .about-fig:not([data-crop]) img {
    max-height: calc(100svh - var(--masthead-h) - 6rem);
    object-fit: contain;
    object-position: left top;
  }
  .about-rail { position: static; margin-bottom: 3.5vh; }
  .about-jump { flex-direction: row; flex-wrap: wrap; column-gap: var(--s5); max-width: none; margin-top: var(--s3); border-top: 0; }
  .about-jump a { min-height: 44px; min-width: 44px; border-bottom: 0; }
}
/* The rail's 32px rows are for a mouse; a finger gets 44 at any width. */
@media (pointer: coarse) {
  .about-jump a { min-height: 44px; }
}

/* ---------- My photography in numbers (About) -------------------------
   Its charts (site.js photoNumbers), in the 图1 Readout's language: three
   blocks, the first and last opened by one large light numeral, small
   dials, then the charts. Mono small-caps labels, slim grey bars on a
   hairline (vertical for years and months, full bars for the hours of the
   day, horizontal for the ranked lists), each vertical bar's rough count
   under its name, and the accent only on what is being read. In a wide box
   each block is two columns (see the end). Sizes in cqi follow the box's
   width, or inside a chart the chart's own (each chart is a container, so
   a chart in a half column sizes itself like a narrow box). */
.photo-numbers {
  container: pnb / inline-size;
  margin: 0 0 var(--s7);
  color: var(--text);
  --b-line: color-mix(in srgb, var(--text) 18%, transparent);
  --b-ink: color-mix(in srgb, var(--text) 58%, transparent);
  --b-dim: color-mix(in srgb, var(--text) 22%, transparent);
  --bw: clamp(3px, 1.5cqi, 10px);   /* a slim vertical bar's width */
  --bh: 3px;                        /* a horizontal bar's height */
  -webkit-tap-highlight-color: transparent;
}
.photo-numbers .nb-block + .nb-block { margin-top: clamp(56px, 13cqi, 96px); }

/* ---- Labels: the site's mono small caps ---------------------------------- */
.photo-numbers :is(.nb-label, .nb-ctx, .nb-k, .nb-title, .nb-tick, .nb-xk, .nb-note) {
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ---- The readout ---------------------------------------------------------- */
.photo-numbers .nb-read { -webkit-user-select: none; user-select: none; }
.photo-numbers :is(.nb-label, .nb-ctx) {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.photo-numbers .nb-label { color: var(--text-body); }
/* The titles, a chart's and a numeral's, a size up from the other labels
   (Newton, 2026-10-16); Chinese a size more, as it is on the rest of the
   page. */
.photo-numbers :is(.nb-title, .nb-label) { font-size: 12px; }
[data-lang="zh"] .photo-numbers :is(.nb-title, .nb-label) { font-size: 13px; }
.photo-numbers .nb-num {
  margin: clamp(6px, 1.6cqi, 12px) 0 clamp(8px, 2cqi, 14px) -.045em;
  font: 300 clamp(44px, 13.5cqi, 84px)/1 var(--sans);
  letter-spacing: -.025em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text);
}

/* A number's unit (张 / photos, 天 / days): small, on its baseline, dimmer. */
.photo-numbers .nb-unit {
  margin-left: .22em;
  font-size: max(13px, .2em);
  font-weight: 400;
  letter-spacing: .02em;
  color: var(--text-dim);
}
.photo-numbers .nb-dv .nb-unit { margin-left: .3em; font-size: 13px; }

/* The dials: a block's headline figures, a size down from the numeral. */
.photo-numbers .nb-dials {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5rem), 1fr));
  gap: 14px clamp(16px, 4cqi, 32px);
  margin-top: clamp(18px, 4.5cqi, 28px);
  padding-top: clamp(12px, 2.6cqi, 16px);
  border-top: 1px solid var(--hairline-soft, var(--b-line));
}
/* With no numeral above them, they open the block. Under a chart (the
   hours of the day), the numeral and dials keep a chart's distance from it. */
.photo-numbers .nb-inst > .nb-dials:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.photo-numbers .nb-chart + .nb-inst { margin-top: clamp(30px, 7cqi, 44px); }
.photo-numbers .nb-dial { display: flex; flex-direction: column; min-width: 0; }
.photo-numbers .nb-dv {
  margin: 4px 0 2px -.03em;
  font: 300 clamp(22px, 4.4cqi, 28px)/1.15 var(--sans);
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  color: var(--text-body);
}
.photo-numbers .nb-note { letter-spacing: .1em; }
/* The biggest trip's one dial, the most in a day (Colombia), is a numeral
   the size of the one above it, its label and note dressed the same
   (Newton, 2026-10-17). */
.photo-numbers .nb-trip .nb-k { font-size: 12px; color: var(--text-body); }
[data-lang="zh"] .photo-numbers .nb-trip .nb-k { font-size: 13px; }
.photo-numbers .nb-trip .nb-dv {
  margin: clamp(6px, 1.6cqi, 12px) 0 clamp(8px, 2cqi, 14px) -.045em;
  font: 300 clamp(44px, 13.5cqi, 84px)/1 var(--sans);
  letter-spacing: -.025em;
  white-space: nowrap;
  color: var(--text);
}
.photo-numbers .nb-trip .nb-dv .nb-unit { margin-left: .22em; font-size: max(13px, .2em); }

/* ---- The charts ------------------------------------------------------------ */
.photo-numbers .nb-chart {
  container: nbc / inline-size;
  margin-top: clamp(30px, 7cqi, 44px);
  -webkit-user-select: none;
  user-select: none;
}
/* A title that has to take two lines (a long one in a half column) keeps
   its own leading, not the prose's. */
.photo-numbers .nb-head { line-height: 18px; padding-bottom: 4px; }
.photo-numbers .nb-title { color: var(--text-body); white-space: normal; line-height: 18px; }

/* Vertical bars, each named and its rough count under the name. */
.photo-numbers .nb-plot {
  display: grid;
  grid-template-columns: repeat(var(--n), minmax(0, 1fr));
  height: clamp(150px, 30cqi, 200px);
  margin-top: clamp(10px, 3cqi, 16px);
}
.photo-numbers .nb-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  outline: none;
  cursor: default;
}
.photo-numbers .nb-well {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  border-bottom: 1px solid var(--b-line);
}
.photo-numbers .nb-bar {
  position: absolute;
  bottom: 0;
  left: calc(50% - var(--bw) / 2);
  width: var(--bw);
  height: calc(var(--v) * 100%);
  background: var(--b-ink);
  transform-origin: 50% 100%;
  transition: background-color .16s var(--ease);
}
.photo-numbers .is-tiny .nb-bar { min-height: 1px; }
.photo-numbers .has-active :is(.nb-bar, .nb-hbar) { background: var(--b-dim); }
.photo-numbers .nb-col.is-active .nb-bar { background: var(--accent); }
.photo-numbers .nb-col.is-zero.is-active .nb-bar { height: 2px; }

/* A name or count wider than its column stays centred on its bar (a flex
   row overflows both ways), except at the two ends, where it is kept
   inside. */
.photo-numbers :is(.nb-tick, .nb-xk) {
  flex: none;
  display: flex;
  justify-content: center;
  padding: 8px 0 0 .14em;
  white-space: nowrap;
  transition: color .16s var(--ease);
}
.photo-numbers .nb-xk {
  padding: 2px 0 0 .04em;
  letter-spacing: .04em;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--text-faint) 80%, transparent);
}
.photo-numbers :is(.nb-tick, .nb-xk) > span { margin: 0 auto; }
.photo-numbers .nb-col:first-child :is(.nb-tick, .nb-xk) { justify-content: flex-start; }
.photo-numbers .nb-col:last-child :is(.nb-tick, .nb-xk) { justify-content: flex-end; }
.photo-numbers .nb-short { display: none; }
.photo-numbers .nb-col.is-active :is(.nb-tick, .nb-xk) { color: var(--accent); }
/* The exact count being read; the rough counts it would run into step
   aside. */
.photo-numbers .nb-col.is-near .nb-xk > span { visibility: hidden; }
.photo-numbers .nb-col:focus-visible .nb-tick > span {
  text-decoration: underline 1px var(--accent);
  text-underline-offset: 5px;
}
.photo-numbers .nb-col:focus-visible .nb-well {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* The hours of the day: a full bar for every hour, every hour named
   ("7 AM"; in a narrower chart the hour alone, with AM and PM at the two
   12s), and on a phone a row each. */
.photo-numbers .nb-hour .nb-plot { height: clamp(160px, 19cqi, 240px); }
.photo-numbers .nb-hour .nb-bar { left: 17%; width: 66%; }
.photo-numbers .nb-hour .nb-tick { letter-spacing: .06em; padding-left: .06em; }
@container nbc (max-width: 959px) {
  .photo-numbers .nb-hour .nb-full { display: none; }
  .photo-numbers .nb-hour .nb-short { display: inline; }
  .photo-numbers .nb-hour .nb-tick { letter-spacing: 0; padding-left: 0; }
}
@container nbc (max-width: 639px) {
  .photo-numbers .nb-hour .nb-plot {
    grid-template-columns: max-content minmax(0, 1fr) max-content;
    column-gap: clamp(10px, 2.6cqi, 18px);
    height: auto;
  }
  .photo-numbers .nb-hour .nb-col {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: center;
    padding: 2px 0;
  }
  .photo-numbers .nb-hour .nb-full { display: inline; }
  .photo-numbers .nb-hour .nb-short { display: none; }
  .photo-numbers .nb-hour :is(.nb-tick, .nb-xk) { padding: 0; }
  .photo-numbers .nb-hour .nb-tick { grid-column: 1; grid-row: 1; justify-content: flex-start; letter-spacing: .1em; }
  .photo-numbers .nb-hour .nb-xk { grid-column: 3; grid-row: 1; justify-content: flex-end; min-width: 6.5ch; }
  .photo-numbers .nb-hour :is(.nb-tick, .nb-xk) > span { margin: 0; }
  .photo-numbers .nb-hour .nb-col:nth-child(n) .nb-tick { justify-content: flex-start; }
  .photo-numbers .nb-hour .nb-col:nth-child(n) .nb-xk { justify-content: flex-end; }
  .photo-numbers .nb-hour .nb-well {
    grid-column: 2; grid-row: 1;
    align-self: stretch;
    min-height: 16px;
    border-bottom: 0;
    border-left: 1px solid var(--b-line);
  }
  .photo-numbers .nb-hour .nb-bar {
    top: 3px; bottom: 3px; left: 0;
    width: calc(var(--v) * 100%);
    height: auto;
    transform-origin: 0 50%;
  }
  .photo-numbers .nb-hour .is-tiny .nb-bar { min-height: 0; min-width: 1px; }
  .photo-numbers .nb-hour .nb-col.is-zero.is-active .nb-bar { width: 2px; height: auto; }
  .photo-numbers .nb-hour .nb-col:focus-visible .nb-well { box-shadow: none; }
  .photo-numbers .nb-hour.nb-pre .nb-bar { transform: scaleX(0); }
}

/* Horizontal bars: the ranked lists. One grid for the whole list (rows are
   subgrids), so every bar starts at the same hairline. */
.photo-numbers .nb-rows {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  column-gap: clamp(10px, 2.6cqi, 18px);
  margin: clamp(8px, 2.4cqi, 12px) 0 0;
  padding: 0;
  list-style: none;
}
.photo-numbers .nb-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  padding: 3px 0;
  outline: none;
  cursor: default;
}
.photo-numbers .nb-row.nb-two {
  row-gap: 1px;
  padding: 7px 0 8px;
  border-top: 1px solid var(--hairline-soft, transparent);
}
.photo-numbers .nb-row.nb-two:first-child { border-top: 0; }
.photo-numbers :is(.nb-p, .nb-s) {
  min-width: 0;
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color .16s var(--ease);
}
.photo-numbers .nb-p { color: var(--text-body); }
.photo-numbers .nb-s { color: var(--text-faint); }
.photo-numbers .nb-n {
  font: 400 var(--t-meta)/16px var(--mono);
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text-body);
  transition: color .16s var(--ease);
}
.photo-numbers .nb-track {
  position: relative;
  align-self: stretch;
  min-height: 16px;
  border-left: 1px solid var(--b-line);
}
.photo-numbers .nb-hbar {
  position: absolute;
  left: 0;
  top: calc(50% - var(--bh) / 2);
  height: var(--bh);
  background: var(--b-ink);
  transform-origin: 0 50%;
  transition: background-color .16s var(--ease);
}
/* Two-line rows: what and count above, where and the bar below. */
.photo-numbers .nb-two .nb-p { grid-column: 1 / 3; grid-row: 1; }
.photo-numbers .nb-two .nb-n { grid-column: 3; grid-row: 1; }
.photo-numbers .nb-two .nb-s { grid-column: 1; grid-row: 2; }
.photo-numbers .nb-two .nb-track { grid-column: 2 / 4; grid-row: 2; }

/* The trips: where and when, and the images, above; how long, how many a
   day and with what under it (either may wrap); the bar across the whole
   row below. Then the list's total. */
.photo-numbers .nb-trips :is(.nb-p, .nb-s) { white-space: normal; overflow: visible; }
.photo-numbers .nb-trips .nb-row.nb-two { row-gap: 2px; padding: 8px 0 9px; }
.photo-numbers .nb-trips .nb-two .nb-p { grid-column: 1 / 3; grid-row: 1; align-self: start; }
.photo-numbers .nb-trips .nb-two .nb-n { grid-column: 3; grid-row: 1; align-self: start; }
.photo-numbers .nb-trips .nb-two .nb-s { grid-column: 1 / -1; grid-row: 2; }
.photo-numbers .nb-trips .nb-two .nb-track { grid-column: 1 / -1; grid-row: 3; min-height: 10px; margin-top: 2px; }
.photo-numbers .nb-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 2px 1em;
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid var(--b-line);
}
.photo-numbers .nb-fv {
  margin-left: auto;
  font: 400 var(--t-meta)/16px var(--mono);
  letter-spacing: .04em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--text-body);
}

/* The busiest days fit on one line in a wide chart: date, place, bar, count. */
@container nbc (min-width: 560px) {
  .photo-numbers .nb-days .nb-rows { grid-template-columns: max-content max-content minmax(0, 1fr) max-content; }
  .photo-numbers .nb-days .nb-row.nb-two { padding: 3px 0; border-top: 0; }
  .photo-numbers .nb-days .nb-two .nb-p { grid-column: 1; grid-row: 1; }
  .photo-numbers .nb-days .nb-two .nb-s { grid-column: 2; grid-row: 1; }
  .photo-numbers .nb-days .nb-two .nb-track { grid-column: 3; grid-row: 1; }
  .photo-numbers .nb-days .nb-two .nb-n { grid-column: 4; grid-row: 1; }
}

.photo-numbers .nb-row.is-active .nb-hbar { background: var(--accent); }
.photo-numbers .nb-row.is-active :is(.nb-p, .nb-n) { color: var(--accent); }
.photo-numbers .nb-row.is-active .nb-s { color: var(--text-body); }
.photo-numbers .nb-row:focus-visible .nb-p {
  text-decoration: underline 1px var(--accent);
  text-underline-offset: 5px;
}

/* ---- Narrow boxes and narrow charts ---------------------------------------- */
/* A narrow chart (a phone, or a half column): years as '16; months closer
   set, then as initials when very narrow. */
@container nbc (max-width: 460px) {
  .photo-numbers .nb-year .nb-full { display: none; }
  .photo-numbers .nb-year .nb-short { display: inline; }
  .photo-numbers :is(.nb-year, .nb-month) .nb-xk { letter-spacing: 0; padding-left: 0; }
}
@container nbc (max-width: 400px) {
  .photo-numbers .nb-month .nb-tick { letter-spacing: .08em; padding-left: .08em; }
}
@container nbc (min-width: 331px) and (max-width: 380px) {
  .photo-numbers .nb-month .nb-tick { letter-spacing: .04em; padding-left: .04em; }
}
@container nbc (max-width: 330px) {
  .photo-numbers .nb-month .nb-full { display: none; }
  .photo-numbers .nb-month .nb-short { display: inline; }
}
/* In Chinese "10月" is wider than "Oct": the month's number alone sooner
   (the title says 每月). */
@container nbc (max-width: 400px) {
  [data-lang="zh"] .photo-numbers .nb-month .nb-full { display: none; }
  [data-lang="zh"] .photo-numbers .nb-month .nb-short { display: inline; }
}
@container nbc (max-width: 300px) {
  .photo-numbers .nb-year .nb-tick { letter-spacing: .06em; padding-left: .06em; }
}
@container pnb (max-width: 460px) {
  .photo-numbers .nb-dials { grid-template-columns: 1fr 1fr; }
  .photo-numbers .nb-dial:nth-child(3):last-child { grid-column: 1 / -1; }
  /* The longest run's note (twice: when and when) takes the room it needs. */
  .photo-numbers .nb-field .nb-dials { grid-template-columns: auto auto; }
}
@container pnb (max-width: 330px) {
  .photo-numbers .nb-dials { grid-template-columns: 1fr; }
}
/* A month's " · all years combined" needs room; narrower, the month alone. */
@container pnb (max-width: 400px) {
  .photo-numbers .nb-suf { display: none; }
}

/* ---- A wide box: two columns per block -------------------------------------- */
/* The section's own width (from the contents list to the text's edge). All
   photos: the numeral and its dials on the left, the countries beside them,
   years beside months below. The hours of the day across both columns,
   then their dials beside the busiest days. The busiest clock hour beside
   the busiest single hours. 680px is WIDE in site.js, which puts the
   country list ahead of the years and months. */
@container pnb (min-width: 680px) {
  .photo-numbers .nb-block {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(40px, 6.5cqi, 72px);
    row-gap: clamp(48px, 6cqi, 64px);
    align-items: start;
  }
  .photo-numbers .nb-block + .nb-block { margin-top: clamp(80px, 10cqi, 120px); }
  .photo-numbers .nb-block > :is(.nb-chart, .nb-inst) { margin-top: 0; }
  .photo-numbers .nb-inst { grid-column: 1; grid-row: 1; }
  .photo-numbers .nb-list { grid-column: 2; grid-row: 1; }
  .photo-numbers .nb-year { grid-column: 1; grid-row: 2; }
  .photo-numbers .nb-month { grid-column: 2; grid-row: 2; }
  /* Days in the field: the numeral and dials beside years over months. */
  .photo-numbers .nb-field .nb-inst { grid-row: 1 / span 2; }
  .photo-numbers .nb-field .nb-year { grid-column: 2; grid-row: 1; }
  .photo-numbers .nb-field .nb-month { grid-column: 2; grid-row: 2; }
  .photo-numbers .nb-day .nb-hour { grid-column: 1 / -1; grid-row: 1; }
  .photo-numbers .nb-day .nb-inst, .photo-numbers .nb-day .nb-list { grid-row: 2; }
  .photo-numbers .nb-num, .photo-numbers .nb-trip .nb-dv { font-size: clamp(56px, 9cqi, 100px); }
  /* Dials at their own widths, wrapping when the column runs out. */
  .photo-numbers .nb-dials {
    display: flex;
    flex-wrap: wrap;
    gap: 16px clamp(24px, 13cqi - 94px, 48px);
  }
  .photo-numbers .nb-plot { height: clamp(120px, 32cqi, 168px); }
  .photo-numbers .nb-hour .nb-plot { height: clamp(170px, 19cqi, 240px); }
}

/* ---- Entrance (once per chart; site.js adds these) ------------------------ */
.photo-numbers .nb-pre .nb-bar { transform: scaleY(0); }
.photo-numbers .nb-pre .nb-hbar { transform: scaleX(0); }
.photo-numbers .nb-grow :is(.nb-bar, .nb-hbar) {
  transition: transform .7s var(--ease-out) calc(var(--i) * 22ms), background-color .16s var(--ease);
}
@media print {
  .photo-numbers .nb-pre :is(.nb-bar, .nb-hbar) { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .photo-numbers .nb-pre :is(.nb-bar, .nb-hbar) { transform: none; }
  .photo-numbers :is(.nb-bar, .nb-hbar, .nb-tick, .nb-xk, .nb-p, .nb-s, .nb-n) { transition: none; }
}

/* It is a section of its own: in the two-column layout its heading and
   charts take the open width from the contents to the text's right edge
   (site.js sets --about-reach), over the stage it has emptied, and it
   starts well below the end of the essay, so the last figure is gone
   before it comes up (Newton, 2026-10-16). */
@media (min-width: 1001px) {
  .about-body.has-figures .about-prose > h2[id^="numbers"],
  .about-body.has-figures .about-prose > .photo-numbers {
    margin-left: calc(-1 * var(--about-reach, 0px));
    position: relative;
    z-index: 1;
  }
  .about-body.has-figures .about-prose > p + h2[id^="numbers"] { margin-top: max(10rem, 36vh); }
}
/* Its title is for the contents list (and a screen reader); on the page
   the hairline alone opens it (Newton, 2026-10-16). */
.about-body .about-prose[data-lang] > h2[id^="numbers"] {
  font-size: 0 !important;
  line-height: 0;
  color: transparent;
  margin-bottom: 0;
}

/* ---------- Projects (projects.html) -----------------------------------
   The prose pages' rail, with projects in the column instead of prose: a
   mono kicker, the name, a blurb, the way in, then its screenshots. The
   apps are light-themed, so each screenshot sits in a hairline frame that
   keeps its edge on the White ground too. */

.projects { min-width: 0; }
.project { scroll-margin-top: calc(var(--masthead-h) + 2rem); }
.project + .project {
  margin-top: var(--s8);
  padding-top: var(--s7);
  border-top: 1px solid var(--hairline);
}

.project-kicker, .project-note, .project-shot figcaption {
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.project-kicker { margin: 0; }

/* Every project is still being built, and says so after its kicker, in the
   accent ("progress"). The dot is drawn here so an emptied status leaves
   no stray separator; the second `content` keeps it from being read out. */
.project-status { color: var(--accent); }
.project-status::before {
  content: "·"; content: "·" / "";
  margin: 0 .55em; color: var(--text-faint);
}
.project-status:empty::before { content: none; }

/* The part being read is lit in the rail (site.js railLights; Projects and
   About): full ink, and the nav's 4×4 accent square hung in the
   gutter to its left so the label doesn't shift as it comes and goes. */
.about-jump a { position: relative; }
.about-jump a[aria-current] { color: var(--text); }
.about-jump a[aria-current]::before {
  content: "";
  position: absolute; right: 100%; top: 50%;
  margin: -2px 8px 0 0;
  border: 2px solid var(--accent);
}

.project-title {
  margin: var(--s3) 0 0;
  font: 500 var(--t-h2)/1.1 var(--sans);
  letter-spacing: -.015em;
  color: var(--text);
}

/* Paragraphs are separated by a blank line, as in a gallery blurb. */
.project-blurb {
  max-width: 62ch;
  margin: var(--s5) 0 0;
  font: 400 var(--t-body)/1.7 var(--sans);
  color: var(--text-body);
  white-space: pre-line;
  text-wrap: pretty;
}

.project-actions {
  display: flex; flex-wrap: wrap; align-items: center;
  column-gap: var(--s5); row-gap: var(--s2);
  margin: var(--s5) 0 var(--s7);
}

/* The figure's width is the column's: the `sizes` hint in projects.html
   (100vw below 1000px, else 62vw up to the 108rem wrap) mirrors it. */
.project-shot { margin: 0 0 var(--s7); }
.project-shot:last-child { margin-bottom: 0; }   /* .about-body pads the end */
.project-shot img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--hairline);
  background: var(--bg-raised);
}
.project-shot figcaption { margin-top: var(--s3); }

/* ==========================================================================
   Edit mode

   None of this is visible to a visitor. It applies only when assets/edit.js
   has put .is-editing on <body>, which only happens with ?edit in the URL.

   Deliberately plain: it is scaffolding around the page, and it should not
   be mistaken for part of the design while you're looking past it at the
   words. The one place it borrows from the site is the mono labels, so the
   bar doesn't read as a browser dialog. Night in both grounds, with an
   amber rule, so it never melts into the page.
   ========================================================================== */

/* The bars are night on White too, so inside them the page's ink tokens
   are pointed back at the night ink; the rules below then read as usual. */
.nl-bar, .nl-format {
  color-scheme: dark;
  --text: var(--bar-text); --text-dim: var(--bar-text-dim);
  --hairline-strong: var(--bar-line); --wash: var(--bar-wash);
  --accent: var(--bar-accent); --accent-hover: var(--bar-accent-hover); --on-accent: var(--bar-on-accent);
}

.nl-bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-bar);
  height: var(--nl-bar-h);
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: 0 var(--edge);
  background: var(--bar);
  border-bottom: 1px solid var(--accent);
  color: var(--text);
}

.nl-bar-label {
  font: 500 var(--t-label)/16px var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--accent);
  flex: none;
}

.nl-bar-hint {
  font: 400 var(--t-ui)/1.4 var(--sans);
  color: var(--text-dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nl-bar-hint.is-bad { color: var(--warn); }

.nl-bar-actions { margin-left: auto; display: flex; gap: var(--s2); flex: none; }

.nl-btn {
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 7px 14px;
  border: 1px solid var(--hairline-strong);
  border-radius: 0;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease),
              opacity var(--d-fast) var(--ease);
}
.nl-btn:hover { background: var(--wash); }
.nl-btn-go { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.nl-btn-go:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.nl-btn:disabled {
  opacity: 0.4; cursor: default;
  background: transparent; border-color: var(--hairline-strong); color: var(--text);
}
.nl-btn-quiet { padding-inline: 10px; font-size: 15px; }

/* The bar sits above everything, so the sticky masthead (and the prose
   rail pinned under it) moves down to meet it rather than sliding
   underneath. */
body.is-editing { padding-top: var(--nl-bar-h); }
body.is-editing .masthead { top: var(--nl-bar-h); }
body.is-editing .about-rail { top: calc(var(--nl-bar-h) + var(--masthead-h) + 2rem); }

/* A slot marked data-edit-lines keeps the line breaks typed into it. The hero
   title is the one that needs this: its break is a deliberate one, and editing
   it as plain text turns the <br> into a newline. Which also means the break
   is now movable without touching any markup. */
[data-edit-lines] { white-space: pre-line; }

/* ---- the editable regions themselves ---------------------------------- */

.is-editing [data-edit] {
  outline: 1px dashed var(--edit-dash);
  outline-offset: 6px;
  border-radius: 0;
  transition: outline-color var(--d-fast) var(--ease);
  cursor: text;
}
.is-editing [data-edit]:hover { outline-color: var(--text-faint); }
.is-editing [data-edit]:focus {
  outline: 1px solid var(--accent);
  outline-offset: 6px;
}

/* An empty slot has no height and nothing to aim at, so it says what it is.
   Most collection subtitles and blurbs start out empty — without this there
   would be nothing on the page to click. */
.is-editing [data-edit]:empty::before,
.is-editing [data-edit][data-edit-hint]:empty::before {
  content: attr(data-edit-hint);
  color: var(--text-faint);
  font-style: italic;
}
.is-editing [data-edit]:empty {
  display: block;
  min-height: 1.4em;
}

/* ---- selection formatting bar ----------------------------------------- */

.nl-format {
  position: absolute;
  z-index: calc(var(--z-bar) + 10);
  display: none;
  gap: 1px;
  padding: 2px;
  background: var(--bar);
  border: 1px solid var(--hairline-strong);
  border-radius: 0;
}
.nl-format.is-on { display: flex; }

.nl-fmt {
  font: 400 var(--t-ui)/1 var(--sans);
  min-width: 2rem;
  padding: 0.4rem 0.45rem;
  border: 0;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  border-radius: 0;
}
.nl-fmt:hover { background: var(--wash); }

/* ---- settings panel ---------------------------------------------------- */

.nl-settings {
  position: fixed;
  z-index: calc(var(--z-bar) + 10);
  top: calc(var(--nl-bar-h) + 0.75rem);
  right: var(--edge);
  width: min(28rem, calc(100vw - 2 * var(--edge)));
  padding: var(--s5);
  background: var(--panel-solid);
  border: 1px solid var(--hairline);
  border-radius: 0;
}
.nl-settings[hidden] { display: none; }

.nl-settings h2 {
  margin: 0 0 var(--s3);
  font: 400 var(--t-label)/16px var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.nl-settings p { margin: 0 0 var(--s4); font: 400 var(--t-ui)/1.6 var(--sans); color: var(--text-dim); }
.nl-settings .nl-fine { font-size: var(--t-meta); color: var(--text-faint); }

.nl-settings input {
  width: 100%;
  font: 400 var(--t-meta)/1.4 var(--mono);
  padding: 10px 12px;
  margin-bottom: var(--s4);
  border: 1px solid var(--hairline);
  border-radius: 0;
  background: var(--bg-raised);
  color: var(--text);
}

.nl-settings-row { display: flex; gap: var(--s2); }

@media (max-width: 640px) {
  .nl-bar-hint { display: none; }
}

/* ==========================================================================
   Chinese. site.js marks every text that is mostly Chinese lang="zh-Hans"
   (Chinese inside English gets a span of its own), including text added
   later, and this sets it in the --zh pairing. The two !importants are there
   because this has to beat each component's own face and tracking, globe.css
   included, which loads after this file. text-autospace leaves a small gap
   between Chinese and Latin (Chrome 140+, Safari 18.4+). Smoothing back on
   auto gives the thin Song strokes a slightly inkier weight on a Mac.
   ========================================================================== */

:lang(zh) {
  font-family: var(--zh) !important;
  letter-spacing: .05em !important;
  text-autospace: normal;
  -webkit-font-smoothing: auto;
}

/* Chinese inside English. The span takes no box of its own, so it can't
   split a flex row (the globe's list) into columns, and with leading 1 its
   different face can't stretch the English line around it. */
[data-zh-run] { display: contents; line-height: 1; }

/* The site in Chinese (中 / EN; <html data-site-lang="zh">, never lang).
   The mono labels are 11px capitals with wide tracking, and Song at 11px
   is too small to read: in Chinese they take About's 13px (the uppercase
   does nothing to characters, and :lang(zh) above sets the tracking). */
:is(.wordmark-sub, .skip-link, .back-link, .book-onward a, .gallery-date, .grid-status, .view-switch a,
    .view-switch [aria-current], .footer-links a,
    .about-jump a, .project-kicker span, .project-note, .project-shot figcaption,
    .g-list-title, .g-region-name, .g-pin-label, .g-card-where, .g-card-date, .g-card-go-label,
    .g-index-link, .g-readout, .g-plate-where, .g-list-status, .g-fallback p, .g-load,
    .text-link, .g-card-time, .g-plate-time, .g-stage .maplibregl-cooperative-gesture-screen):lang(zh) { font-size: 13px; }
/* Contact's labels in Chinese end in a full-width ：, which brings its own space. */
:root[data-site-lang="zh"] [data-edit="contact.prose"] .contact-list li { column-gap: 0; }
/* The readout's 北纬 / 西经 are spans in the mono line, the digits left as they are. */
.g-readout [lang|="zh"] { font-size: 13px; }
/* A Chinese line of places breaks only between places, never inside a name. */
.g-plate-where:lang(zh), .g-card-where:lang(zh) { word-break: keep-all; }
/* The bird's Chinese name, before the English and the Latin. */
.lb-zh { margin-right: .7em; font-size: 13px; }

/* Until site.js has put the page's own words into Chinese (it does before
   the first paint on a normal load), they stay hidden rather than flash
   English; if it never runs, they show anyway after 1.5 s. */
:root[data-site-lang="zh"]:not([data-i18n-ready]) :is(.nav, .wordmark-sub, .footer, .back-link,
    .gallery-title, .grid-status, .g-list-head, .g-list-status, .view-switch, .project-actions,
    [data-edit]:not([data-edit-rich])) {
  visibility: hidden;
  animation: nl-show 0s 1.5s forwards;
}
@keyframes nl-show { to { visibility: visible; } }

/* The punctuation Chinese shares with English (the · in a name, the ——
   dash, the …… ellipsis) would otherwise come from American Typewriter,
   which sets …… low on the line and breaks —— in two. This lends just those
   characters, inside --zh only, a Song face, or a Mincho one (iPhone) when
   no Song face is installed. */
@font-face {
  font-family: "ZH Punct";
  src: local("Songti SC"), local("STSongti-SC-Regular"), local("STSong"),
       local("Noto Serif CJK SC"), local("NotoSerifCJKsc-Regular"),
       local("Source Han Serif SC"), local("SourceHanSerifSC-Regular"),
       local("Hiragino Mincho ProN W3"), local("HiraMinProN-W3"), local("SimSun");
  unicode-range: U+00B7, U+2014-2015, U+2026;
}

/* ==========================================================================
   Forced colours (Windows High Contrast and the like): the system palette
   takes over. Outlines stay; the brackets, which are background images,
   go, since they would keep their own colours.
   ========================================================================== */

@media (forced-colors: active) {
  .bk, .bk-hover::after,
  .grid.book .tile::after, .collection-card img,
  .lb-close::after, .lb-nav::after { background-image: none; }
}

/* ==========================================================================
   No page transitions. A cross-document view transition (`@view-transition
   { navigation: auto }`) can't live in this file: Chrome settles whether
   the arriving page takes part when it reaches <body>, and on a normal load
   this stylesheet is still on its way then, so the fade was skipped and the
   console logged an InvalidStateError on most clicks between pages. To have
   the fade, put that rule in an inline <style> in the <head> of every page
   (PAGE_TEMPLATE in tools/build.py included), not here.
   ========================================================================== */
