@font-face {
  font-family: "Bricolage Grotesque";
  src: url("fonts/bricolage-grotesque.woff2") format("woff2");
  font-style: normal;
  font-weight: 200 800;
  font-stretch: 75% 100%;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("fonts/ibm-plex-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 100 700;
  font-stretch: 75% 100%;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/ibm-plex-mono-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/ibm-plex-mono-bold.woff2") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

/* Subset covers only the glyphs used on this page (bilingual details). */
@font-face {
  font-family: "Noto Serif SC";
  /* pinned: _headers serves /assets/fonts/* as immutable for a year, so a
     returning visitor holds the OLD subset until 2027 unless the URL changes.
     Adding the 干支 glyphs on 2026-08-19 is exactly the case that needs it —
     without the pin those readers would see the date fall back to a system
     font, or to tofu on a machine with no CJK serif. Bump with the others. */
  src: url("fonts/noto-serif-sc-subset.woff2?v=49") format("woff2");
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
}

/* Qin Builds 立轴 — the hanging scroll.
   One fixed, full-viewport living ink painting; the page is a mounted
   scroll travelling over it. Content sits on 留白 (reserved-white)
   washes, stations are vertical inscriptions, and the scroll ends in
   the 题跋 colophon where the reader adds their own words.
   纸 paper · 墨 pine ink · 朱 cinnabar · 青 celadon */
:root {
  --paper: #f3f2ea;
  --paper-raised: #faf9f2;
  --paper-deep: #eae8dc;
  /* 裱绫 mount silk — the 惊燕 strips at the head. Left as a live color-mix of
     two tokens that swap per theme, so it needs no 科技 override: it is always
     a few steps off the page ground in whichever direction that ground runs. */
  --silk: color-mix(in srgb, var(--ink) 9%, var(--paper));
  --ink: #222a22;
  --ink-soft: #3c453c;
  /* — 题跋 dusk: the ground the scroll descends into, and the type on it.

     These exist as their OWN tokens rather than reusing --ink / --paper
     because those two swap places between themes, and this one rule must
     not swap with them (Baichuan, 2026-08-18: "is this on purpose to have
     reversed color?"). Written as `background: var(--ink)` the colophon
     descended into ink dusk on 纸墨 and *ascended* into a full-bleed
     #e8ede4 on 科技 — a near-white flood arriving after ~3,500px of night.
     Contrast was never the problem; the problem was that the section's own
     description, "the scroll descends into ink dusk", stopped being true in
     one of the two themes.

     Both themes end by getting darker. But "darker" alone left 纸墨 ending in
     ink — a dark band with light type at the foot of an otherwise all-paper
     page — and Baichuan's call on 2026-08-19 was "light use light, dark use
     dark at the contact area". So the foot now also stays inside its own
     theme's family: it descends WITHOUT flipping polarity. On 纸墨 that means
     a deeper sheet rather than a dark one, which is what a 立轴 actually does —
     the painting proper is mounted on 裱绫, a deeper paper, not on ink.

     #e0dbc8 is 1.235:1 from --paper and 1.34:1 from the 宣纸 sheet directly
     above it, which is the edge a reader actually sees. Deeper than
     --paper-deep, which is a mid-page surface token and would not read as an
     ending. Both invariants are asserted in test/colophon-dusk.test.js.

     CAUTION when touching anything in this section: every derived value inside
     .contact is an alpha of currentColor, and a given alpha buys far less
     contrast dark-on-light than light-on-dark — .62 measured 6.00:1 on the old
     ink foot and only 3.81:1 here. The alphas in .contact were re-floored
     against THIS ground; they were not carried over. */
  --dusk: #e0dbc8;
  --dusk-ink: #222a22;
  /* --ink-mute carries real reading matter (the process rail, the header
     line, every mono label), so it is a contrast token, not a decorative
     one: 6.3:1 on paper. The old #6d7568 measured 4.3:1 — under the 4.5:1
     AA floor before the moving wash behind it was even counted. */
  --ink-mute: #525b50;
  --line: #d8d5c6;
  --line-dark: #b9b6a5;
  --cinnabar: #c6472e;
  --cinnabar-deep: #a2371f;
  --celadon: #a9bfad;
  --celadon-deep: #48604c;
  --grid: 8px;
  --space-1: calc(var(--grid) * .5);
  --space-2: var(--grid);
  --space-3: calc(var(--grid) * 1.5);
  --space-4: calc(var(--grid) * 2);
  --space-5: calc(var(--grid) * 3);
  --space-6: calc(var(--grid) * 4);
  --space-7: calc(var(--grid) * 6);
  --space-8: calc(var(--grid) * 8);
  --radius-sm: 6px;
  --line-thin: 1px;
  --line-strong: 2px;
  --control-height: 48px;
  --z-skip-link: 20;
  --z-rail: 6;
  --font-display: "Bricolage Grotesque", "Avenir Next", sans-serif;
  --font-body: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;
  --font-cjk: "Noto Serif SC", "Songti SC", "SimSun", serif;
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  /* 宣纸 holder: the sheet the text is written on. a laid-on piece of
     rice paper, not a card — its edges feather into the painting rather
     than boxing the text. --rice is the sheet color, --rice-o its core
     opacity, --rice-fiber the strength of the paper grain; the tech theme
     overrides all three so the sheet becomes a dark clearing under light
     type.

     The sheet must be VISIBLE. At 246,245,238 it was three units off the
     page paper — a backing nobody could see, so the type read as floating
     on the wash (review 2026-08-17). It now sits clearly above both the
     page and the wash, and carries grain, so a reader sees the leaf the
     text is written on. */
  --rice: 253, 251, 245;
  /* NOT opaque. One sheet now covers the whole scroll, so an opaque sheet
     would delete the painting from the page entirely. At .90 the ink soaks
     THROUGH the paper the way 水墨 actually behaves, and it is safe only
     because uInkFloor caps how dark the wash can get: paper over the very
     darkest possible ink still measures 5.8:1 for --ink-mute and 5.6:1 for
     the seal marks. The floor and this opacity are one design, not two. */
  --rice-o: .90;
  --rice-fiber: .5;
  /* how the sheet lifts off the painting. A drop-shadow applied AFTER the
     blur follows the deckle, so this softens the silhouette rather than
     drawing a box around it — which is why the dark theme keeps a shadow
     too even though nothing there needs "lifting". */
  --rice-lift: rgba(34, 42, 34, .09);
  /* 帘纹: the lines the bamboo screen leaves in handmade 宣纸 — closely spaced
     laid lines and the wider chain lines that cross them. This is the paper's
     actual signature, and until 2026-08-19 the sheet only approximated it by
     stretching isotropic turbulence 2.13:1, which yields elongated blobs
     rather than lines. Kept at roughly one grey level: the sheet's own history
     records that a straight line reads far more readily than the ~1.5 levels
     of random grain around it, which is exactly why this works at all — and
     exactly why it becomes a UI grid if pushed. Measure, do not eyeball. */
  --laid: color-mix(in srgb, var(--ink) 1.1%, transparent);
  --chain: color-mix(in srgb, var(--ink) 2%, transparent);

  /* 杆: the scroll's two rods are cut from ONE piece of wood. 天杆 and 地杆
     differ in PROFILE — flat vs round, knobless vs knobbed — never in
     material. They used to mix from different pairs against different grounds
     (--ink/--paper at the head, --dusk-ink/--dusk at the foot) and measured
     ~20 grey levels and a hue apart: two different woods (Baichuan, 2026-08-19:
     "these 2 bars don't match"). One pair feeds both now, and each simply sits
     on whatever ground it lands on, which is what real hardware does.
     --rod is the shadowed underside, --rod-lit the lit upper face. */
  --rod: color-mix(in srgb, var(--ink) 36%, var(--paper));
  --rod-lit: color-mix(in srgb, var(--ink) 15%, var(--paper));

  /* — 留白: how the text column sits inside the 宣纸 —
     The content width DERIVES from the sheet width; the two are not
     independent numbers. They used to be (`100% - 32px` capped 1180 for the
     column, `100% - 8px` capped 1272 for the sheet), which meant the gap
     between them collapsed to a fixed 12px at every viewport under ~1290 —
     and since the sheet only reaches full opacity --sheet-fade in from its
     edge, the text actually began INSIDE the feathered deckle at every
     width (measured 2026-08-17: −8px of solid paper at 1440, −42px at 1200,
     −50px at 390). Text on a scroll needs paper under it, not fade.

     --page-gutter is the real margin from the sheet edge to the first
     glyph; the solid paper a reader sees beyond the text is
     (--page-gutter − --sheet-fade), so keep the gutter comfortably larger
     than the fade. --measure is shared by .container and .scroll-head so
     the header always aligns with the copy. */
  --sheet-max: 1272px;
  --sheet-fade: 40px;
  /* the head of the sheet: it starts --sheet-head-rise ABOVE <main> (up under
     the header veil) and its mask runs from transparent to solid over
     --sheet-head-fade. Solid paper therefore begins
     (--sheet-head-fade − --sheet-head-rise) below main's top — the line
     anything stamped ON the paper has to clear. Tokens rather than the
     literals they replaced in .scroll-sheet::before, because .seal--lead
     derives its position from them and a silent edit to one would put the
     chop back in the feather. */
  --sheet-head-rise: 40px;
  --sheet-head-fade: 96px;
  --page-gutter: 72px;
  --page-max: 1100px;
  --measure: min(
    calc(min(100% - var(--space-2), var(--sheet-max)) - var(--page-gutter) * 2),
    var(--page-max)
  );
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  text-rendering: optimizeLegibility;
}
a { color: inherit; }
a:focus-visible, button:focus-visible { outline: 3px solid var(--cinnabar); outline-offset: 3px; border-radius: 2px; }
button { font: inherit; }
::selection { background: var(--cinnabar); color: var(--paper); }
.container { width: var(--measure); margin-inline: auto; }

/* mono labels: .72rem with .12em tracking put ~11px of loosely-spread type
   on a living background — the first thing that went unreadable. .78rem at
   .1em keeps the same quiet register and stays legible. */
.meta, .project-type, .step-index {
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .1em;
  text-transform: lowercase;
}
.meta { color: var(--ink-mute); }

.skip-link { position: fixed; z-index: var(--z-skip-link); top: var(--space-2); left: var(--space-2); padding: var(--space-2) var(--space-3); background: var(--ink); color: var(--paper); border-radius: var(--radius-sm); transform: translateY(-260%); }
.skip-link:focus { transform: translateY(0); }

/* — the painting: fixed, full-viewport, always alive — */
.ink-stage { position: fixed; inset: 0; z-index: 0; }
.ink-stage canvas, .art-fallback { position: absolute; inset: 0; width: 100%; height: 100%; }
/* static wash for no-JS and reduced motion */
.art-fallback { background:
  radial-gradient(42% 55% at 47% 30%, rgba(34, 42, 34, .14), rgba(34, 42, 34, 0) 70%),
  radial-gradient(30% 38% at 62% 62%, rgba(34, 42, 34, .10), rgba(34, 42, 34, 0) 70%),
  radial-gradient(16% 22% at 36% 70%, rgba(198, 71, 46, .12), rgba(198, 71, 46, 0) 70%),
  radial-gradient(20% 26% at 58% 22%, rgba(72, 96, 76, .10), rgba(72, 96, 76, 0) 70%); }

/* the mounted scroll travels over the painting. pools bleed past the
   container on purpose; clip so they never cause horizontal scroll. */
.mount { position: relative; z-index: 1; overflow: clip; }

/* — 宣纸 holder: a laid sheet of rice paper under the content, so copy
   stays readable wherever the living ink drifts.

   Built as a SOLID fill with an irregular radius, then blurred: the
   centre is fully opaque under the text and only the edge feathers,
   like a deckle. Do NOT build these from radial-gradients — an ellipse
   whose radius exceeds its own box gets cut off square, which is
   exactly the card edge this design removed (caught in review,
   2026-08-16). A blurred shape cannot clip.

   A sheet is a RECTANGLE with deckled corners, never an ellipse. The
   ~50%% radii these started with produced amorphous cloud shapes that
   read as accidental erasures in the wash rather than as paper, and they
   could not reach their own corners (Baichuan, 2026-08-17: "don't use
   these shapes directly on the background"). Absolute radii keep the
   deckle organic while still covering the box; the blur feathers it. — */
.rice { position: relative; isolation: isolate; }
.rice::before {
  content: "";
  position: absolute;
  inset: -14% -9%;
  z-index: -1;
  pointer-events: none;
  background: rgba(var(--rice), var(--rice-o));
  border-radius: 88px 118px 74px 104px / 44px 32px 54px 38px;
  /* blur first, THEN drop-shadow: the shadow follows the blurred deckle
     instead of a hard box, so the sheet lifts off the painting without
     ever showing a card edge. */
  filter: blur(26px) drop-shadow(0 12px 20px var(--rice-lift));
}
/* the grain. Two feTurbulence fields — fine fibre and a slow cloud
   mottle, which is what 宣纸 actually looks like — inset far enough to
   sit inside the fully-opaque core of the sheet above, and radially
   masked so the texture has no boundary of its own. The gradient mask is
   allowed here where it is forbidden for the sheet itself: this layer
   carries grain, not contrast, so washing out toward the corners is the
   desired behaviour rather than a defect. */
.rice::after {
  content: "";
  position: absolute;
  inset: -8% -5%;
  z-index: -1;
  pointer-events: none;
  opacity: var(--rice-fiber);
  background-image:
    /* 帘纹, on top of the fibre so the lines stay crisp rather than muddied.
       Horizontal, matching the lay the stretched noise below already implies —
       and vertical lines on a vertically-scrolling page fight the scroll and
       start to read as rules. Soft-edged rather than a hard 1px band, which
       would alias into moiré at fractional device-pixel ratios. ~5px laid
       spacing and ~96px chain spacing are 宣纸's real proportions at screen
       scale (roughly 1.3mm and 2.5cm). */
    repeating-linear-gradient(0deg, transparent 0, var(--laid) 1.1px, transparent 2.4px, transparent 5px),
    repeating-linear-gradient(0deg, transparent 0, var(--chain) 1.4px, transparent 3px, transparent 96px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.36'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='520' height='520'%3E%3Cfilter id='c' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.011' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='520' height='520' filter='url(%23c)' opacity='.5'/%3E%3C/svg%3E");
  /* the fine tile is stretched wide rather than square: 宣纸 fibre lies in
     the sheet, so stretching isotropic turbulence horizontally reads as
     laid fibre instead of sensor noise. The cloud field stays square.

     Both filters carry an explicit x/y/width/height region. That is load-
     bearing, not decoration: `stitchTiles="stitch"` can only stitch when the
     filter region matches the tile exactly, and with the default region
     (-10%, 120%) it silently does not. The result was straight seams every
     520px — only ~2 grey levels, but the eye reads a straight line far more
     readily than the ~1.5 levels of random grain around it, so the paper
     looked gridded (review 2026-08-17). Measured at x=661 and x=1181. */
  /* aligns by index with background-image: the two 帘纹 gradients tile
     themselves and take auto; the fine fibre tile is the stretched one. */
  background-size: auto, auto, 320px 150px, 520px 520px;
  /* closest-side is load-bearing: the falloff must complete exactly AT the
     box edge or the texture shows a hard rectangle — the same ellipse-
     exceeds-its-box trap codified for the sheets, hit twice here in review
     2026-08-17. Note the gotcha that caused the second hit: PERCENTAGE radii
     in a radial-gradient resolve against the full box width/height, NOT the
     half-extents, so `100% 100%` builds an ellipse twice the size of the box
     and is still ~80% opaque where the box ends. It looked fine on the light
     sheet, where the grain only darkens by a unit or two, and was obvious on
     the tech theme, where grey grain over a near-black clearing lightens by
     nine. Use the keyword, not percentages. */
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 38%, transparent 100%);
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 38%, transparent 100%);
}
/* — 整幅宣纸: ONE sheet for the whole scroll (Baichuan, 2026-08-17: "we
   need a whole piece of 宣纸"). Per-station leaves read as a stack of
   separate panels; a hanging scroll is one piece of paper. Applied to
   <main>, so it runs from the hero to the end of 题跋, whose own ink
   gradient covers the sheet's foot.

   The deckle is horizontal-only here. A full-scroll sheet is thousands of
   pixels tall, and `filter: blur()` on it would rasterise one enormous
   layer; the two side edges are the only ones ever seen, since the head
   sits under the header veil and the foot under the contact ink. Linear
   gradients cannot clip the way an ellipse can, so the codified
   no-gradient-pools rule does not apply to them. — */
.scroll-sheet::before {
  top: calc(var(--sheet-head-rise) * -1);
  bottom: 0;
  left: 50%;
  right: auto;
  width: min(100% - var(--space-2), var(--sheet-max));
  transform: translateX(-50%);
  border-radius: 0;
  filter: none;
  background: linear-gradient(90deg,
    rgba(var(--rice), 0) 0,
    rgba(var(--rice), var(--rice-o)) var(--sheet-fade),
    rgba(var(--rice), var(--rice-o)) calc(100% - var(--sheet-fade)),
    rgba(var(--rice), 0) 100%);
  /* feather the head too, so the sheet has no hard horizontal start where
     the header veil has already faded out */
  mask-image: linear-gradient(180deg, transparent 0, #000 var(--sheet-head-fade));
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 var(--sheet-head-fade));
}
.scroll-sheet::after {
  top: 0;
  bottom: 0;
  left: 50%;
  right: auto;
  width: min(100% - var(--space-4), calc(var(--sheet-max) - 56px));
  transform: translateX(-50%);
  /* the inherited radial mask is wrong at this scale: `closest-side` on a
     box thousands of pixels tall gives the grain a vertical falloff, so how
     much paper texture you see depends on where you happen to be in the
     scroll. The grain runs the full height at even strength and feathers
     only sideways, matching the sheet's own side fade.

     …except at the very head, where it had a HARD edge. `top: 0` switched the
     texture on instantly at the sheet's top while the sheet itself feathers in
     over --sheet-head-fade, and that mismatch measured a −3.8 grey-level step
     across 5px at y≈89 — four times steeper than the 题跋 fade this site spent
     a round smoothing, and visible as a seam under the header (Baichuan's
     screenshot, 2026-08-19). Isolated by disabling one layer at a time:
     neither the veil nor the sheet moved it; removing the grain removed it.

     The second mask layer feathers the head over exactly the sheet's own head
     fade, and it is a SMOOTHSTEP rather than a two-stop ramp — a linear
     gradient starts at full slope, so it trades a hard edge for a visible
     knee, which is the lesson the colophon fade already carries. Composited
     with `intersect` so the side feather still applies. */
  mask-image:
    linear-gradient(90deg, transparent 0, #000 40px,
                    #000 calc(100% - 40px), transparent 100%),
    linear-gradient(180deg,
      rgba(0, 0, 0, 0)    0,
      rgba(0, 0, 0, .028) calc(var(--sheet-head-fade) * .1),
      rgba(0, 0, 0, .104) calc(var(--sheet-head-fade) * .2),
      rgba(0, 0, 0, .216) calc(var(--sheet-head-fade) * .3),
      rgba(0, 0, 0, .352) calc(var(--sheet-head-fade) * .4),
      rgba(0, 0, 0, .5)   calc(var(--sheet-head-fade) * .5),
      rgba(0, 0, 0, .648) calc(var(--sheet-head-fade) * .6),
      rgba(0, 0, 0, .784) calc(var(--sheet-head-fade) * .7),
      rgba(0, 0, 0, .896) calc(var(--sheet-head-fade) * .8),
      rgba(0, 0, 0, .972) calc(var(--sheet-head-fade) * .9),
      rgba(0, 0, 0, 1)    var(--sheet-head-fade));
  mask-composite: intersect;
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, #000 40px,
                    #000 calc(100% - 40px), transparent 100%),
    linear-gradient(180deg,
      rgba(0, 0, 0, 0)    0,
      rgba(0, 0, 0, .028) calc(var(--sheet-head-fade) * .1),
      rgba(0, 0, 0, .104) calc(var(--sheet-head-fade) * .2),
      rgba(0, 0, 0, .216) calc(var(--sheet-head-fade) * .3),
      rgba(0, 0, 0, .352) calc(var(--sheet-head-fade) * .4),
      rgba(0, 0, 0, .5)   calc(var(--sheet-head-fade) * .5),
      rgba(0, 0, 0, .648) calc(var(--sheet-head-fade) * .6),
      rgba(0, 0, 0, .784) calc(var(--sheet-head-fade) * .7),
      rgba(0, 0, 0, .896) calc(var(--sheet-head-fade) * .8),
      rgba(0, 0, 0, .972) calc(var(--sheet-head-fade) * .9),
      rgba(0, 0, 0, 1)    var(--sheet-head-fade));
  -webkit-mask-composite: source-in;
}

/* — 目录 rail: horizontal graduations down the left margin —

   Restored 2026-08-17 ("we need a vertical selector for faster scrolling —
   the horizontal lines we had earlier"). It was retired with the 立轴
   redesign as chrome the scroll didn't need; on a page this long it turned
   out to be navigation, not chrome. This is the tick-line variant, not the
   dot-on-a-spine one that replaced it — a horizontal graduation reads as a
   measure mark on the roller rather than as a carousel dot.

   Ticks are always visible; the label appears for the current section and
   on hover, so the rail is a position indicator at rest and a menu when you
   reach for it. The label needs the chip treatment because, like the station
   inscriptions, it sits on the painting with no sheet under it.

   Shown only from 1024px up: below that the sheet is effectively full-bleed
   and the left margin is the text's own gutter, so the rail would sit on top
   of the copy. The header nav covers navigation there. — */
.toc-rail {
  position: fixed;
  z-index: var(--z-rail);
  left: clamp(10px, 1.6vw, 28px);
  top: 50%;
  display: none;
  grid-auto-flow: row;
  gap: 2px;
  transform: translateY(-50%);
  /* the rail is fixed, so it travels the whole scroll while its tick stays
     --ink-mute. Since 2026-08-19 the 题跋 foot no longer inverts against the
     page, so the rail meets only one polarity per theme and the tick reads
     unaided — but the halo stays, because the ground it crosses for most of
     that travel is the living wash, not a flat colour, and --rice is always
     the opposite of the text colour in either theme. */
  filter: drop-shadow(0 0 4px rgba(var(--rice), .85));
}
.toc-rail a {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 26px;
  color: var(--ink-mute);
  text-decoration: none;
}
/* the graduation itself */
.toc-rail a::before {
  content: "";
  flex: none;
  width: 14px;
  height: var(--line-thin);
  background: currentColor;
}
.toc-rail a span {
  padding: 3px 10px;
  border: var(--line-thin) solid var(--line-dark);
  border-radius: 999px;
  background: var(--paper-raised);
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: lowercase;
  white-space: nowrap;
  opacity: 0;
  transform: translateX(-4px);
}
.toc-rail a:hover { color: var(--ink); }
.toc-rail a:hover span,
.toc-rail a:focus-visible span,
.toc-rail a[aria-current] span { opacity: 1; transform: none; }
.toc-rail a[aria-current] { color: var(--cinnabar-deep); }
.toc-rail a[aria-current]::before { width: 26px; height: var(--line-strong); }
.toc-rail a[aria-current] span { color: var(--cinnabar-deep); font-weight: 700; }
@media (min-width: 1024px) { .toc-rail { display: grid; } }

/* — 天杆 stave: the hardware that closes the head of the scroll —
   Asymmetric to .roller ON PURPOSE. A 立轴 hangs from a flat stave and is
   weighted by a round, knobbed rod at the foot, because it rolls up from the
   bottom; knobs at both ends would be a 手卷. So this is flatter (6px vs 12px),
   squarer (2px radius vs 999px) and knobless, while sharing the roller's width
   and its light-from-above gradient so the two read as one piece of joinery.

   It mixes from --ink/--paper rather than the roller's --dusk pair, because it
   sits on the page ground rather than on the 地头, and those tokens invert per
   theme exactly as the stave should. z-index lifts it over .scroll-head::before,
   the header's veil, which reaches 140px above the header and would otherwise
   wash it out. */
/* Both rods span the SHEET, not the text measure. They were sized off
   .container (1068px at 1440) while the 宣纸 is --sheet-max (1272px), so the
   paper hung 102px past each rod end — a rod narrower than the paper it holds
   cannot exist, and it also left the 引首章 stamped 46px outside the stave's
   right end. Same expression as .scroll-sheet::before so the two cannot drift;
   the 轴头 knobs then protrude past the paper, which is correct. */
.stave, .roller { width: min(100% - var(--space-2), var(--sheet-max)); margin-inline: auto; }
.stave-mount { position: relative; z-index: 1; padding-top: var(--space-3); }
.roller-mount { position: relative; }
.stave {
  height: 6px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--rod), var(--rod-lit) 70%);
}

/* — 天头 header: quiet mono line floating at the top of the scroll — */
.scroll-head { position: relative; isolation: isolate; display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: var(--space-5); padding: var(--space-4) 0; width: var(--measure); margin-inline: auto; }
/* full-bleed veil behind the header. it must reach the true viewport
   edges and fade over a long distance — an inset box with a short
   gradient shows a visible horizontal seam where it ends (found in
   review, tech theme, 2026-08-16). 100vw + the negative left offset
   escapes the centered container without touching document width,
   because .mount clips overflow. */
.scroll-head::before {
  content: "";
  position: absolute;
  top: -140px;
  bottom: -120px;
  left: 50%;
  width: 100vw;
  z-index: -1;
  transform: translateX(-50%);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--paper) 90%, transparent) 0%,
    color-mix(in srgb, var(--paper) 90%, transparent) 58%,
    color-mix(in srgb, var(--paper) 52%, transparent) 78%,
    color-mix(in srgb, var(--paper) 0%, transparent) 100%);
  pointer-events: none;
}
/* 惊燕 "startled swallows": the two free-hanging strips pasted at the head of a
   mount, originally to flutter birds off the silk, later kept as mounting
   vocabulary. Drawn as two backgrounds on ONE pseudo-element rather than two
   elements, since a pseudo can only make one box and the pair is a single
   ornament. z-index -1 puts them inside the header's isolated stacking context
   BEHIND the text but above the veil (equal z-index, later in paint order), so
   they never reduce the contrast of anything readable — measured, not assumed.
   The bottom is masked rather than cut: a hard edge here would read as two grey
   rectangles, which is the card vocabulary this design deleted. Shown from
   1024px only, where the header is a single row with a clear field between the
   status line and the nav; below that it wraps and there is nowhere to hang. */
.scroll-head::after { content: none; }
@media (min-width: 1024px) {
  .scroll-head::after {
    content: "";
    position: absolute;
    /* 0 is exactly the stave's underside: .stave-mount is padding-top plus
       stave height and ends where .scroll-head begins, so the strips hang FROM
       the rod instead of crossing it — they overlapped it by 12px until
       2026-08-19. If .stave-mount's padding changes, re-measure this. */
    top: 0;
    left: 50%;
    z-index: -1;
    /* 10 × 104 ≈ 1:10. At 15 × 74 the pair read as two grey bars rather than
       ribbons — a hanging strip is recognised by its slenderness, so the
       proportion is the whole effect. The 132px span keeps them close enough
       to read as one paired ornament instead of two separate marks, and lands
       both in the clear field between the status line and the nav. */
    width: 132px;
    height: 104px;
    transform: translateX(-50%);
    background-image:
      linear-gradient(var(--silk), var(--silk)),
      linear-gradient(var(--silk), var(--silk));
    background-size: 10px 100%, 10px 100%;
    background-position: 0 0, 100% 0;
    background-repeat: no-repeat;
    mask-image: linear-gradient(180deg, #000 0 54%, transparent 100%);
    -webkit-mask-image: linear-gradient(180deg, #000 0 54%, transparent 100%);
    pointer-events: none;
  }
}
.head-name { font-family: var(--font-display); font-size: 1.02rem; font-weight: 700; letter-spacing: -.01em; text-decoration: none; }
.head-now { margin: 0; color: var(--ink-mute); font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; }
.head-now a { color: var(--ink-soft); text-underline-offset: 3px; }
.head-now a:hover { color: var(--cinnabar-deep); }
.head-nav { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.head-nav a { padding-block: 4px; color: var(--ink-soft); font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; text-decoration: none; white-space: nowrap; }
.head-nav a:hover { color: var(--cinnabar-deep); }

/* — 引首 hero: the leading seal opens the scroll, type settles at the base —

   The head of the hero used to be dead wash. `justify-content: flex-end` on a
   full-viewport frame pinned the headline to the floor and left ~300px of
   empty painting above it (Baichuan, 2026-08-17: "top area is kinda empty").
   The first pass capped the frame and used `space-between` to hand the head
   to the 引首章. Once the chop moved onto the paper itself (`.seal--lead`,
   absolutely positioned against <main>) the hero no longer has to reserve
   that band at all, so the cap tightened again — 560px instead of 720px —
   and the type went back to settling at the base. — */
/* The cap sets the 天头. Content is pinned to flex-end, so this height IS the
   blank approach above the painting — padding-top does nothing here while the
   block is shorter than the frame.
   天大地小: a 立轴 carries a large blank head and a shorter foot, so the eye
   makes a long approach into the painting and lands rather than slides off.
   Measured 2026-08-19 at 1440, from the 天杆 to the h1 against the last
   content to the 地杆: it was 99 : 161 = 0.61, i.e. inverted. Now 159 : 105 =
   1.51, the classic 六四 proportion. Restoring 2:1 was possible but would have
   cost another ~60px of head, and an empty head is exactly what was cut back
   on 2026-08-17; the difference now is that the head is no longer empty — the
   天杆 and its 惊燕 occupy it, which is what a 天头 is for. */
.hero-frame { position: relative; display: flex; flex-direction: column; justify-content: flex-end; min-height: min(calc(100vh - 150px), 700px); min-height: min(calc(100svh - 150px), 700px); padding-top: var(--space-5); padding-bottom: clamp(var(--space-5), 5vh, var(--space-7)); }
.hero-block { max-width: 62rem; }
.hero h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.7rem, 7.3vw, 6.7rem);
  font-weight: 640;
  letter-spacing: -.036em;
  line-height: .99;
  text-wrap: balance;
}
.hero h1 em { font-style: normal; color: var(--cinnabar); }
.hero h1 em:last-of-type { color: var(--celadon-deep); }
.hero-intro { max-width: 30rem; margin: var(--space-5) 0 0; color: var(--ink-soft); font-size: clamp(1.05rem, 1.5vw, 1.25rem); line-height: 1.5; }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }
.button { display: inline-flex; align-items: center; justify-content: center; min-height: var(--control-height); padding: 0 var(--space-4); border: var(--line-thin) solid transparent; border-radius: var(--radius-sm); color: var(--paper); font-weight: 640; text-decoration: none; }
.button--primary { background: var(--cinnabar); }
.button--primary:hover { background: var(--cinnabar-deep); }

/* quiet invitation to stir the canvas */
.hero-hint {
  position: absolute; right: 0; bottom: var(--space-4);
  display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 5px 12px;
  /* capsule, matching .contact-status: these two are the site's only quiet
     informational chips and were shaped differently for no reason — this one
     radius-sm, that one 999px. One chip vocabulary. */
  background: var(--paper-raised); border: var(--line-thin) solid var(--line-dark); border-radius: 999px;
  color: var(--ink-mute); font-family: var(--font-mono); font-size: .76rem; letter-spacing: .1em; text-transform: lowercase;
  pointer-events: none;
}
.hero-hint b { font-family: var(--font-cjk); font-size: .95rem; font-weight: 600; color: var(--cinnabar); }

/* — 印章: the stamped chops.

   Cut from the public-domain 说文解字 小篆 outlines, not from a font and not
   drawn by hand: the 小篆 form of 秦 is 廾 over 禾 and shares almost nothing
   with the modern 楷书 shape, so anything that starts from the regular-script
   glyph produces a character that does not exist. Authoring source lives in
   design/elements/.

   Mounted as a MASK over a background colour rather than as <img>. Three
   reasons, all load-bearing: the chop then inherits the theme's cinnabar
   token instead of baking one hex into the file, `style-src 'self'` forbids
   the inline fill an inlined SVG would want, and the mark stays one cached
   asset instead of ~16 KB of path data in every HTML response.

   This revives the seal system retired 2026-08-16, deliberately and with the
   rule that killed it intact: what failed then was 秦-as-logo, repeated
   across favicon, map pins, status chops and a watermark. There are exactly
   two chops now — a 闲章 that opens the scroll and a 名章 that signs it — and
   neither is a brand mark. Do not add a third without a reason a scroll would
   recognise. — */
.seal {
  display: block;
  flex: none;
  background-color: var(--cinnabar);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
/* 引首章 一半春秋　一筆冬夏 — 2×4, replacing the 1×4 有容乃大 on 2026-08-19.
   有容乃大 is 林则徐's line and anyone may stamp it; this one states how the
   name 秦百川 is assembled out of the four seasons (春 gives its head and 秋
   its 禾 to make 秦, 夏 sheds one 横 to make 百, 冬 takes one more stroke to
   make 川), so it is a stone nobody else has a use for. That construction is
   a 楷书 fact produced by 隶变 and is NOT legible in this 小篆 cut — the stone
   states the sentence, and the 名章 at 题跋 is the answer to it.

   The file is `seal-lead.svg`, not a rewrite of `seal-idle.svg`: mask URLs
   carry no `?v=` pin, so replacing a mask's CONTENTS at the same path would
   serve returning visitors the old chop out of cache. A different stone gets
   a different path. It is wider than the 1×4 it replaces (1016/1935 vs
   620/1900), so the width clamp came down to hold the rendered height in the
   same band rather than letting the chop grow into the text column. */
.seal--idle { width: clamp(56px, 5.4vw, 78px); aspect-ratio: 1016 / 1935; -webkit-mask-image: url("seal-lead.svg"); mask-image: url("seal-lead.svg"); }
/* `.seal--name` / `.seal--sign` (名章 秦百川, square, beside the contact brush
   stroke) were removed 2026-08-19 at Baichuan's request, along with
   `assets/seal-name.svg` and the retired `assets/seal-idle.svg`. The 名章 cut
   survives as the apple-touch-icon and in `design/elements/`; only its
   on-page mount is gone. The 题跋 now closes on the brush stroke alone. */

/* The chop is unglossed (Baichuan, 2026-08-17). The site's rule that every
   CJK element carries an adjacent English reading is about CJK *type* — a
   carved chop is a mark on the paper, and captioning it turns a signature
   into a diagram. The reading lives in `aria-label`, so the page still says
   what the seal says to anyone who cannot see it. */
/* 引首章 placement: stamped on the top-right corner of the 宣纸 itself, not
   set inside the hero's text column (Baichuan, 2026-08-17). Taking it out of
   the hero's flow is what allows the hero to stop reserving height for it —
   the chop now sits in the paper margin beside the headline instead of above
   it.

   Both insets clear the sheet's FEATHER, not its geometric edge — a chop is
   pressed onto paper, and paper that is still fading is not yet paper
   (Baichuan, 2026-08-18: "有容乃大 still too close to 宣纸 edge"). The
   previous values tracked the geometric edge and landed the chop inside both
   deckles at once: --space-4 (16px) sat 24px OUTSIDE the solid side edge,
   which begins --sheet-fade (40px) in, and a 12–24px top sat 32px above the
   solid head line at --sheet-head-fade − --sheet-head-rise (56px).

   Right: `.scroll-sheet::before` is `width: min(100% - var(--space-2),
   var(--sheet-max))` centred, so its inset from <main> is
   `max(var(--space-1), (100% - var(--sheet-max)) / 2)`. Add --sheet-fade to
   reach solid paper, then --space-4 of visible margin beyond it. The chop's
   left edge sits ~118px in from the sheet edge while the text column's own
   margin is 86px, so the chop DOES overlap the column band by about 32px —
   an earlier version of this note claimed the opposite by reading the
   inequality backwards. It does not collide because .hero-frame is
   `justify-content: flex-end`, so the top of the column is empty where the
   chop sits. If the hero ever becomes top-aligned, this is the first thing
   that breaks. */
.seal--lead {
  position: absolute;
  z-index: 1;
  top: calc(var(--sheet-head-fade) - var(--sheet-head-rise) + var(--space-3));
  right: calc(max(var(--space-1), (100% - var(--sheet-max)) / 2)
              + var(--sheet-fade) + var(--space-4));
}

/* — the two-sided statement: one build has a customer face and a system
   face. Formerly two blurred colour blobs (Baichuan, 2026-08-17: "we don't
   need these color blocks"). They were the last amorphous shapes on the
   page, and once the whole scroll became one sheet of 宣纸 there was nothing
   left for them to be painted ON — a saturated cinnabar ellipse sitting on
   rice paper reads as a sticker, not as ink. Now the pair is plain type
   under two rules, and the lens emphasis that the colour used to carry
   moves into type size and weight. — */
.stage-band { padding: clamp(var(--space-7), 9vh, calc(var(--space-8) * 1.4)) 0; }
.hero-stage { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(var(--space-5), 5vw, var(--space-8)); }
.stage-face { padding-top: var(--space-4); border-top: var(--line-strong) solid var(--cinnabar-deep); }
.stage-face--system { border-top-color: var(--celadon-deep); }
.stage-face strong {
  display: block;
  max-width: 20ch;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.7vw, 2.5rem);
  /* three short sentences: without balance the muted side broke as
     "…Keep / it.", orphaning two characters */
  text-wrap: balance;
  font-weight: 620;
  letter-spacing: -.026em;
  line-height: 1.06;
  color: var(--ink);
}
/* the de-emphasised side recedes by type, not by desaturation: there is no
   fill left to desaturate */
[data-lens="business"] .stage-face--system strong,
[data-lens="tech"] .stage-face--choice strong {
  font-size: clamp(1.2rem, 1.9vw, 1.7rem);
  color: var(--ink-mute);
}

/* — stations: each section opens with a vertical inscription — */
.station-section { padding: clamp(var(--space-7), 9vh, calc(var(--space-8) * 1.4)) 0; }
.station-grid { display: grid; grid-template-columns: clamp(52px, 7vw, 96px) minmax(0, 1fr); gap: var(--space-4); align-items: start; }
.station { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); margin: 0; }
.station b {
  writing-mode: vertical-rl;
  font-family: var(--font-cjk);
  font-size: clamp(1.7rem, 2.9vw, 2.5rem);
  font-weight: 700;
  letter-spacing: .34em;
  /* the seal marks measured 4.31:1 on paper as --cinnabar: technically
     AA-large, but these are hairline CJK serif strokes and Baichuan could
     not read 笔法 (review 2026-08-17). --cinnabar-deep is 6.03:1 and, because
     the cinnabar pair inverts between themes, it is correspondingly LIGHTER
     on the tech ground — one token change fixes both. */
  color: var(--cinnabar-deep);
  /* the vertical inscriptions are the one piece of type with no sheet under
     it — they sit directly on the painting by design. A single soft shadow
     at 70% could not hold cinnabar against a dark wash, so the halo is
     stacked: two opaque paper glows tight to the strokes, one soft falloff
     to feather it back into the painting. */
  text-shadow: 0 0 9px rgba(var(--rice), 1), 0 0 17px rgba(var(--rice), 1),
               0 0 30px rgba(var(--rice), .78);
}
.station span { writing-mode: vertical-rl; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .26em; text-transform: lowercase; color: var(--ink-mute); text-shadow: 0 0 8px rgba(var(--rice), 1), 0 0 14px rgba(var(--rice), 1); }
.contact .station span { text-shadow: none; }
.station::after { content: ""; width: var(--line-strong); flex: 1 0 34px; min-height: 34px; margin-top: var(--space-2); background: linear-gradient(180deg, var(--cinnabar-deep) 0%, color-mix(in srgb, var(--cinnabar-deep) 0%, transparent) 100%); border-radius: 2px; }

.section-title { max-width: 17ch; margin: 0; font-family: var(--font-display); font-size: clamp(2.1rem, 4.5vw, 4.1rem); font-weight: 630; letter-spacing: -.03em; line-height: 1.03; text-wrap: balance; }
.section-lede { max-width: 34rem; margin: var(--space-4) 0 0; color: var(--ink-soft); font-size: 1.08rem; }

/* — work: open compositions, text wash beside an ink vignette — */
.work-list { display: grid; gap: clamp(var(--space-7), 8vh, calc(var(--space-8) * 1.2)); margin-top: var(--space-7); }
#work[data-lens="business"] .project[data-business-order="1"], #work[data-lens="tech"] .project[data-tech-order="1"] { order: 1; }
#work[data-lens="business"] .project[data-business-order="2"], #work[data-lens="tech"] .project[data-tech-order="2"] { order: 2; }
.project { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .9fr); gap: clamp(var(--space-5), 4vw, var(--space-8)); align-items: center; }
.project--flip .project-content { order: 2; }
.project--flip .project-visual { order: 1; }
.project-content { display: flex; flex-direction: column; align-items: flex-start; }
.project-topline { display: flex; gap: var(--space-4); }
.project-type { color: var(--celadon-deep); font-weight: 700; }
.project h3 { margin: var(--space-3) 0; font-family: var(--font-display); font-size: clamp(2.1rem, 4vw, 3.5rem); font-weight: 630; letter-spacing: -.03em; line-height: 1; }
.project-summary { max-width: 30rem; margin: 0; color: var(--ink-soft); font-size: 1.05rem; }
.project-footer { margin: var(--space-4) 0 0; }
.project-link { display: inline-flex; align-items: center; gap: var(--space-1); min-height: 44px; color: var(--cinnabar-deep); font-weight: 640; text-decoration-thickness: var(--line-strong); text-underline-offset: 5px; }
.project-link:hover { color: var(--ink); }

/* 画心 plates: the project visuals are MOUNTED on the paper leaf, not
   dropped straight onto the painting (Baichuan, 2026-08-17: "don't use
   these shapes directly on the background"). This is a deliberate, scoped
   exception to pools-not-cards, and it is truer to the 立轴 metaphor than
   what it replaces: a hanging scroll mounts its painting proper as a
   defined panel inside the paper. As free-floating blurred ellipses these
   read as ink stains that happened to land where the artwork was — and
   because the radius was ~50%, the ellipse could not cover its own box, so
   the map labels and code lines ran off the dark ground at the corners.
   Only these plates are boxed; every text sheet stays deckled. */
/* 5:3, not 5:4, since 2026-08-18: every plate on this site now holds a
   Scene Distillation leaf at the skill's landscape ratio, so the work
   plates, the 笔法 plate and the 山水 leaf are one measure. The ink ground
   is gone with the CSS specimens it used to back — a leaf brings its own
   paper, and an ink fill behind it would only show as a hairline. The
   装裱 rule and cast shadow stay: they are what mounts the plate ON the
   scroll instead of letting it float. */
.vignette, .approach-frame {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  aspect-ratio: 5 / 3;
  /* the ink ground is what the plate shows while a lazily-loaded leaf is still
     arriving. Dropping it left an empty transparent box carrying a cast shadow
     with nothing casting it — for as long as a multi-megabyte leaf takes to
     land, which is not a theoretical interval. It never shows once the image
     is in: 969 × 5 = 4845 = 1615 × 3, so the leaves are exactly 5:3 and
     `contain` fills the box with no letterbox. */
  background: var(--ink);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--celadon) 24%, transparent),
              0 16px 38px rgba(34, 42, 34, .20);
}
.vignette img, .approach-frame img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* — the evidence, moved out of the plates (Baichuan, 2026-08-18).

   The Venus tagline and the seatbelt profile used to be typeset inside the
   visuals as aria-hidden decoration. They are the section's actual proof —
   "One build, two kinds of proof" — so they are live text now: selectable,
   translatable, spoken by a screen reader, and restyleable.

   Deliberately NOT a card. No fill, no border box, no radius: a single
   hairline rule on the leading edge, the way a 题跋 annotation sits beside
   the work rather than in its own frame. Adding a background here would
   put back the exact object this change removed. */
.project-evidence {
  margin: var(--space-4) 0 0;
  padding-left: var(--space-4);
  border-left: var(--line-strong) solid var(--celadon-deep);
}
.evidence-claim {
  display: grid;
  gap: 3px;
  margin: 0;
}
.evidence-claim b {
  font-family: var(--font-cjk);
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--ink);
}
.evidence-claim span {
  font-family: var(--font-mono);
  /* .78rem / .1em is the documented floor for mono labels on the living
     background — see the note above .meta. These glosses arrived at .7rem /
     .18em, inherited verbatim from .vmap-tagline span, where they were
     aria-hidden decoration on a high-contrast ink plate. They are real reading
     matter on 宣纸 now, so they take the floor. */
  font-size: .78rem;
  letter-spacing: .1em;
  color: var(--ink-mute);
}
/* the places read as one line of unequal beats, the way they sit on the
   leaf — a list with bullets or an even grid would turn a diaspora into a
   directory */
.evidence-set {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-3) 0 0;
}
.evidence-set span { display: grid; gap: 2px; }
.evidence-set b {
  font-family: var(--font-cjk);
  font-size: .95rem;
  font-weight: 700;
  color: var(--ink-soft);
}
.evidence-set i {
  font-family: var(--font-mono);
  /* same floor. .58rem / .18em was 9.28px of loosely-tracked mono — smaller and
     more tracked than the .72rem / .12em the stylesheet already recorded as
     "the first thing that went unreadable". */
  font-size: .78rem;
  font-style: normal;
  letter-spacing: .1em;
  color: var(--ink-mute);
}
/* the profile keeps its own line breaks and stays selectable; it scrolls
   rather than wraps, because a re-wrapped seatbelt rule is a different
   rule and this one has to stay copy-pasteable */
.evidence-code { margin: 0; overflow-x: auto; }
.evidence-code code {
  font-family: var(--font-mono);
  font-size: clamp(.7rem, 1vw, .82rem);
  line-height: 1.85;
  color: var(--ink-soft);
  white-space: pre;
}

/* — 笔法 approach: mounted tea-room painting with quieter principles — */
#approach .station-body {
  display: grid;
  gap: clamp(var(--space-6), 5vh, var(--space-7));
}
#approach .station-head { max-width: 38rem; }
.approach-spread {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: clamp(var(--space-5), 4vw, var(--space-8));
  align-items: start;
}
.approach-painting {
  margin: 0;
  min-width: 0;
}
/* The 笔法 plate is the same 画心 treatment as the work plates and is declared
   with them above — it was a verbatim six-declaration copy until 2026-08-18,
   and the two copies had already drifted (the work plates lost their ink
   ground while this one kept it). Shared now, so they cannot disagree again. */
.approach-copy {
  display: grid;
  gap: var(--space-5);
  min-width: 0;
}
.approach-invitation {
  margin: 0;
  max-width: 28rem;
  padding-top: var(--space-3);
  border-top: var(--line-thin) solid var(--line-dark);
  color: var(--ink-mute);
  font-family: var(--font-mono);
  font-size: .92rem;
  letter-spacing: .01em;
  line-height: 1.45;
}
.services {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  margin: 0;
}
/* .method-open shares every one of these: it is a fourth principle in 笔法 that
   happens to be installable, not a work card that wandered in, so it reads in
   the same voice. Shared rather than copied — the 画心 plate above is the
   standing reminder that a verbatim duplicate drifts. */
.service,
.method-open {
  padding-top: var(--space-3);
  border-top: var(--line-strong) solid var(--ink);
}
.service h3,
.method-open h3 {
  margin: var(--space-3) 0 var(--space-2);
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1.8vw, 1.45rem);
  font-weight: 620;
  letter-spacing: -.02em;
  line-height: 1.1;
}
.service p,
.method-open-summary {
  max-width: 28rem;
  margin: 0;
  color: var(--ink-soft);
}
/* capped on the block itself, unlike .service, which inherits its measure from
   the narrow copy column. This one spans the station body. No fill, no box, no
   radius — the top rule is the whole treatment. */
/* min-width: 0 is load-bearing, not tidying. As a grid item .method-open would
   otherwise take min-width: auto — its own min-content — and the install
   command is `white-space: pre`, so that min-content is the full 40-character
   line. At 390 that widened the whole 笔法 column from 290 to 380 and pushed
   .service and .process-rail 90px past the viewport. It was invisible to a
   scrollWidth check, because .mount { overflow: clip } swallows the overflow
   rather than scrolling — a clip ancestor makes documentElement.scrollWidth
   useless as an overflow test, and rendered right edges must be measured
   instead. With this, the pre's own overflow-x: auto finally does its job. */
.method-open { max-width: 38rem; min-width: 0; }
/* Deliberately NO `.method-open code` rule. One existed for an inline <code>
   in the summary; the summary no longer has one, and the rule outlived it —
   `.method-open code` and `.evidence-code code` have equal specificity, so the
   later of the two won and silently blew the install command from its clamped
   .7rem up to 14.72px, which is what pushed it off a 390 screen. If inline
   code ever returns here, scope it (`.method-open-summary code`) so it cannot
   reach into the pre. */
/* The install command is 40 characters and lands exactly 4px over the content
   width at 390 — and a tail clipped by 4px reads as a rendering bug, not as a
   scrollable region. The type is already at its floor, so the block's own
   leading inset gives the room back instead (16px → 8px, leaving 4px spare).
   Scoped to .method-open, so the #work seatbelt profile keeps its inset. Below
   ~360 it genuinely scrolls, which is correct: a re-wrapped command is a
   different command. */
@media (max-width: 420px) {
  .method-open .project-evidence { padding-left: var(--space-2); }
}
/* the four skills under the repo's own two category names. Same unequal-beats
   shape as .evidence-set — a bulleted list or an even grid would turn four
   skills into a directory — but NOT that rule reused: its names set in
   --font-cjk because they were place names, and these are identifiers you
   would type. Every mono size here is the documented .78rem/.1em floor. */
.skill-index { margin: var(--space-4) 0 0; }
.skill-index dt {
  font-family: var(--font-mono);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: lowercase;
  color: var(--celadon-deep);
}
.skill-index dt + dd { margin-top: var(--space-2); }
.skill-index dt ~ dt { margin-top: var(--space-3); }
.skill-index dd {
  display: flex;
  flex-wrap: wrap;
  /* wider column gap than .evidence-set's --space-4: there the names were two
     CJK glyphs and always narrower than their gloss, so the beats stayed
     separate on their own. Here a gloss is routinely wider than the name above
     it ("fresh worktree" vs wt-start), and at --space-4 the three glosses ran
     together into one line of prose. */
  gap: var(--space-2) var(--space-5);
  margin: 0;
}
.skill-index dd > span { display: grid; gap: 2px; }
.skill-index b { font-family: var(--font-mono); font-size: .9rem; font-weight: 700; color: var(--ink-soft); }
.skill-index i {
  font-family: var(--font-mono);
  font-size: .78rem;
  font-style: normal;
  letter-spacing: .1em;
  color: var(--ink-mute);
}
.process-rail {
  margin-top: 0;
  padding-top: var(--space-4);
  border-top: var(--line-thin) solid var(--line-dark);
}
.process-list {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3);
  padding: 0;
  margin: 0;
  list-style: none;
}
.process-step {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
  color: var(--ink-mute);
  font-size: .95rem;
}
.process-step .step-index { color: var(--ink-mute); }
.process-step strong { color: var(--ink); }
.process-step--active .step-index { color: var(--cinnabar); font-weight: 700; }

/* — 落款 about — */
.about-grid { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(230px, 1fr); gap: clamp(var(--space-6), 5vw, var(--space-8)); }
.about-copy { max-width: 44rem; font-size: clamp(1.15rem, 1.7vw, 1.45rem); letter-spacing: -.012em; line-height: 1.45; }
.about-copy p { margin: var(--space-4) 0 0; color: var(--ink-soft); }
.about-aside { align-self: start; padding-top: var(--space-3); border-top: var(--line-strong) solid var(--celadon-deep); }
.about-aside ul { display: grid; gap: var(--space-2); padding: 0; margin: var(--space-3) 0 0; list-style: none; color: var(--ink-soft); }
.about-aside li { padding-bottom: var(--space-2); border-bottom: var(--line-thin) solid var(--line-dark); }
/* the album leaf, mounted under the background list. Named `.about-leaf`
   and not `.about-photo` since 2026-08-18: the Yosemite photograph was
   replaced by a Scene Distillation 山水 print, and a selector that still
   said "photo" would be the only thing in this file lying about what it
   holds. The 1615×969 source renders at ~319px here — roughly 5× down, so
   the dry-print grain stays crisp with no srcset needed. */
.about-leaf { margin: var(--space-5) 0 0; }
.about-leaf img { display: block; width: 100%; height: auto; border: var(--line-thin) solid var(--line-dark); border-radius: var(--radius-sm); }
.about-leaf figcaption { margin-top: var(--space-2); color: var(--ink-mute); font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; line-height: 1.6; }
.about-leaf b { color: var(--celadon-deep); font-family: var(--font-cjk); font-weight: 700; letter-spacing: .18em; }

/* — 题跋 contact: the scroll descends into its own dusk — */
/* 题跋: the scroll descends into dusk — deeper paper on 纸墨, deeper night on
   科技. It gets darker in both themes without flipping which way round it
   reads; see --dusk in :root. The fade is a SMOOTHSTEP, not a
   linear ramp. A two-stop linear gradient starts at full slope, so where it
   meets the flat paper above there is a slope discontinuity — measured as
   flat 237 for 200px and then an immediate -15 per 20px, which reads as a
   hard band however long you make it (review 2026-08-17: "the fade isn't
   smooth"). These ten stops trace t²(3-2t), whose slope is zero at BOTH
   ends, so the fade has no visible beginning and no visible landing.
   Lengthening a linear gradient does not fix a knee; only easing does.

   The stops are in px so the fade always completes in 400px, and padding-top
   is floored at 300px so the copy never lands on part-mixed ink — at 300px
   the ground is already 93% ink. margin-top is 0 on purpose: the fade region
   now provides the breathing room that the margin used to, so the space is
   doing work instead of sitting dead. */
.contact {
  position: relative;
  margin-top: 0;
  padding-top: clamp(300px, 32vh, 440px);
  /* the 地头. Shorter than the 天头 by design — see .hero-frame. */
  padding-bottom: var(--space-6);
  color: var(--dusk-ink);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--dusk)   0%, transparent) 0,
    color-mix(in srgb, var(--dusk)   3%, transparent) 40px,
    color-mix(in srgb, var(--dusk)  10%, transparent) 80px,
    color-mix(in srgb, var(--dusk)  22%, transparent) 120px,
    color-mix(in srgb, var(--dusk)  35%, transparent) 160px,
    color-mix(in srgb, var(--dusk)  50%, transparent) 200px,
    color-mix(in srgb, var(--dusk)  65%, transparent) 240px,
    color-mix(in srgb, var(--dusk)  78%, transparent) 280px,
    color-mix(in srgb, var(--dusk)  90%, transparent) 320px,
    color-mix(in srgb, var(--dusk)  97%, transparent) 360px,
    var(--dusk) 400px);
}
.contact .station b { text-shadow: none; }
.contact .station span { color: color-mix(in srgb, currentColor 75%, transparent); }
.contact-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, .8fr); gap: clamp(var(--space-6), 5vw, var(--space-8)); align-items: end; }
.contact h2 { max-width: 16ch; margin: var(--space-3) 0 0; font-family: var(--font-display); font-size: clamp(2.1rem, 4.4vw, 3.9rem); font-weight: 620; letter-spacing: -.028em; line-height: 1.05; text-wrap: balance; }
.contact p { max-width: 30rem; color: color-mix(in srgb, currentColor 78%, transparent); }

/* availability: a status fact, styled as a chip */
.contact-status { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 5px 12px; border: var(--line-thin) solid color-mix(in srgb, currentColor 38%, transparent); border-radius: 999px; color: color-mix(in srgb, currentColor 80%, transparent); font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; }
.contact-status i { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--celadon); }

/* the brush mark under the heading: a filled tapered stroke revealed
   through a dash-animated mask centerline. without JS (no data-enhanced)
   it renders complete; the draw fires once when the section arrives. */
.contact-stroke { display: block; width: clamp(200px, 52%, 330px); height: auto; margin: var(--space-3) 0 0; color: var(--cinnabar); }
.cs-reveal { stroke-dasharray: 1; }
html[data-enhanced] .contact:not(.is-inked) .cs-reveal { stroke-dashoffset: 1; }

/* — elsewhere: ruled rows — */
.elsewhere { display: grid; margin: var(--space-5) 0 0; padding: 0; list-style: none; max-width: 22rem; }
.elsewhere li { display: flex; align-items: baseline; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: var(--line-thin) solid color-mix(in srgb, currentColor 34%, transparent); }
/* these were opacity .55 / .38 over the ink ground — 3.4:1 and 2.3:1 — then
   .72 / .62 once measured. Raw opacity is not a contrast decision, and it is
   not even a stable one: the SAME alpha buys a different ratio on each ground,
   because dark-on-light loses far more of it than light-on-dark. When the foot
   moved from ink to deep paper (2026-08-19) the ordinal fell 6.00:1 → 3.81:1
   on nothing but a token change. Re-floored against the light foot, which is
   now the binding ground: .80 → 6.26:1 here / 10.75:1 on 科技, .70 → 4.72:1
   here / 8.31:1 on 科技. The gap between them is the hierarchy, deliberately
   widened so it survives being re-floored again. */
.elsewhere span { font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; opacity: .80; }
.elsewhere li > span:first-child { opacity: .70; }
.elsewhere a { font-weight: 640; text-underline-offset: 4px; }
.elsewhere li > span:last-child { margin-left: auto; }

/* — contact form: posts to the worker; no address on the page — */
.contact-form { display: grid; gap: var(--space-3); }
.form-field { display: grid; gap: var(--space-1); }
.form-field span { font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: lowercase; opacity: .85; }
/* 55%, not 40%: an input's border IS its affordance, so it owes the 3:1
   non-text floor. At 40% it measured 3.37:1 on the old ink foot — already
   marginal — and 2.21:1 on the deep-paper foot, which fails. 55% gives
   3.16:1 here and 5.04:1 / 5.44:1 on the two darker grounds. */
.form-field input, .form-field textarea { padding: var(--space-2) var(--space-3); border: var(--line-thin) solid color-mix(in srgb, currentColor 55%, transparent); border-radius: var(--radius-sm); background: transparent; color: inherit; font: inherit; }
.form-field input:focus-visible, .form-field textarea:focus-visible { border-color: currentColor; outline: none; }
.form-field textarea { resize: vertical; min-height: 110px; }
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.form-status { margin: 0; font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; }
.form-submit { justify-self: start; border: 0; cursor: pointer; }
.form-submit:disabled { opacity: .6; cursor: default; }

/* — 地头 footer: the scroll closes on its roller — */
.site-footer { padding: var(--space-3) 0 var(--space-6); background: var(--dusk); color: color-mix(in srgb, var(--dusk-ink) 78%, transparent); }
.footer-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; padding-bottom: var(--space-3); }
.footer-format { margin: 0; font-family: var(--font-mono); font-size: .72rem; letter-spacing: .12em; text-transform: lowercase; }
.footer-format b { font-family: var(--font-cjk); font-size: .8rem; font-weight: 700; letter-spacing: .2em; color: color-mix(in srgb, var(--cinnabar-deep) 82%, var(--dusk-ink)); }
/* 干支 in the 落款. Written in INK, not cinnabar: on a real work the date is
   part of the signature the brush writes, and the red belongs to the chops
   alone — the 名章 is already a few centimetres away. The gloss the CJK rule
   requires rides beside it in the same mono register as the rest of .meta,
   with the browser's default italic removed. Both strings come from site.js,
   computed from the same year as the copyright. */
.ganzhi { font-family: var(--font-cjk); font-size: .82rem; font-weight: 700; letter-spacing: .16em; }
.ganzhi-gloss { margin-left: .45em; font-style: normal; opacity: .8; }
.design-strip { display: grid; grid-template-columns: repeat(4, 1fr); width: 264px; max-width: 100%; overflow: hidden; border: var(--line-thin) solid color-mix(in srgb, currentColor 38%, transparent); border-radius: 999px; }
.spec-band { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 3px; padding: 5px 0 6px; }
/* hairline cell dividers between adjacent bands.
   The strip shows the four literal palette tokens on whatever ground the footer
   happens to be, so in EACH theme one band lands nearly on that ground. WHICH
   band moves with the dusk decision: it was 墨 #222a22 while 纸墨 ended in ink
   (an exact 1.00:1 match), and it is 纸 #f3f2ea at 1.235:1 now that the foot is
   deep paper; on 科技 it is still 夜 #10150f at 1.08:1. What bounds such a band
   is the strip's own outer border on the outside plus these dividers on the
   inside — the first band has no preceding sibling, so `+` alone could never
   have been the whole answer. Both are load-bearing; neither is decoration. */
.spec-band + .spec-band { border-left: var(--line-thin) solid color-mix(in srgb, currentColor 38%, transparent); }
.spec-band b { font-family: var(--font-cjk); font-weight: 700; font-size: .8rem; line-height: 1; }
.spec-band span { padding: 0 6px; font-family: var(--font-mono); font-size: .5rem; letter-spacing: .09em; line-height: 1; }
.spec-band--paper b::before { content: "纸"; }
.spec-band--paper span::before { content: "paper"; }
.spec-band--ink b::before { content: "墨"; }
.spec-band--ink span::before { content: "ink"; }
.spec-band--cinnabar b::before { content: "朱"; }
.spec-band--cinnabar span::before { content: "cinnabar"; }
.spec-band--celadon b::before { content: "青"; }
.spec-band--celadon span::before { content: "celadon"; }
.spec-band--paper { background: var(--paper); color: var(--ink); }
.spec-band--ink { background: var(--ink); color: var(--paper); }
.spec-band--cinnabar { background: var(--cinnabar); color: var(--paper); }
.spec-band--celadon { background: var(--celadon); color: var(--ink); }

/* the 轴 roller bar with its two end knobs */
/* the 轴 is the hardware that closes the scroll, so it has to be an object,
   not a suggestion. Its mixes were 22%/30%, floored against the ink foot where
   they bought 1.96:1 and 2.51:1; on the deep-paper foot the same numbers
   collapsed to 1.51:1 and 1.77:1 and the bar read as a smudge. Lifted uniformly
   rather than per-theme — 科技's roller was the faintest of the three at 1.78:1
   and gains by the same correction. Now 2.11:1 on 纸墨 and 3.11:1 on 科技. */
.roller { position: relative; height: 12px; border-radius: 999px; background: linear-gradient(180deg, var(--rod), var(--rod-lit) 70%); }
/* 轴头: the turned ends of the same rod. Lit from the upper left like the bar
   is lit from above, so the highlight is the LIGHTER stop at 34% 30% and the
   shadow falls away to --rod — the previous gradient ran dark-centre to
   light-edge, which reads as a hole rather than a sphere. */
.roller::before, .roller::after { content: ""; position: absolute; top: 50%; width: 22px; height: 22px; border-radius: 50%; transform: translateY(-50%); background: radial-gradient(circle at 34% 30%, var(--rod-lit), var(--rod) 78%); }
.roller::before { left: -14px; }
.roller::after { right: -14px; }

@media (max-width: 900px) {
  .project, .project--flip { grid-template-columns: 1fr; gap: var(--space-4); }
  .project--flip .project-content { order: 1; }
  .project--flip .project-visual { order: 2; }
  .about-grid, .contact-grid, .approach-spread { grid-template-columns: 1fr; }
}

@media (max-width: 780px) {
  /* the gutter and the fade both scale down together: on a phone the sheet
     is nearly full-bleed, so a desktop-sized 72px margin would eat a fifth
     of the reading width. Override the TOKENS, never .container's width —
     a competing width declaration is what let the column drift outside the
     sheet's solid core in the first place. */
  :root { --page-gutter: 20px; --sheet-fade: 12px; }
  .scroll-head { grid-template-columns: 1fr auto; row-gap: var(--space-3); }
  .head-now { display: none; }
  /* span both columns: the nav shares row 2 with nothing, but leaving it in
     the 1fr column left it only (measure − theme pill − gap) ≈ 208px, which
     wrapped "contact" onto a third row once the column narrowed for the
     宣纸 gutters */
  .head-nav { grid-row: 2; grid-column: 1 / -1; gap: var(--space-3); }
  .theme-col { grid-row: 1; grid-column: 2; }
  .theme-button i { display: none; }
  .hero-frame { min-height: min(calc(100vh - 150px), 520px); min-height: min(calc(100svh - 150px), 520px); }
  /* the sheet runs edge to edge on a phone, so the chop tucks into the
     corner rather than tracking a deckle that is no longer inset — but it
     still has to clear the (narrower, 12px) side feather. --space-1 of sheet
     inset + --sheet-fade + --space-2 lands its right edge exactly on the copy
     gutter, so on a phone the chop and the column share one margin instead of
     fighting for a band only 8px wide. */
  .seal--lead { right: calc(var(--space-1) + var(--sheet-fade) + var(--space-2)); }
  .station-grid { grid-template-columns: 40px minmax(0, 1fr); gap: var(--space-3); }
  .station b { font-size: 1.45rem; letter-spacing: .26em; }
  .station span { display: none; }
  /* the square override is gone with the CSS specimens (2026-08-18). Those
     were fixed compositions whose labels and pins collided at phone width,
     so they needed extra vertical room and a whole respaced layout; a leaf
     is one image and holds its 5:3 at every width. */
  .process-list { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--space-4); }
  .hero-stage { grid-template-columns: 1fr; gap: var(--space-5); }
}

@media (max-width: 460px) {
  .hero h1 { font-size: clamp(2.2rem, 10.5vw, 2.8rem); }
  .footer-inner { align-items: flex-start; flex-direction: column; }
}

/* — motion: the painting is the motion; type arrives like ink soaking in — */
@media (prefers-reduced-motion: no-preference) {
  .stage-face strong { transition: font-size 320ms var(--ease-out), color 320ms var(--ease-out); }
  body, .theme-button { transition: background-color 320ms var(--ease-out), color 320ms var(--ease-out), border-color 320ms var(--ease-out); }
  .hero h1, .hero-intro, .hero-actions { animation: soak 900ms var(--ease-out) both; }
  .hero h1 { animation-delay: 60ms; }
  .hero-intro { animation-delay: 220ms; }
  .hero-actions { animation-delay: 340ms; }
  .button { transition: background-color 180ms var(--ease-out), border-color 180ms var(--ease-out), color 180ms var(--ease-out); }
  .cs-reveal { transition: stroke-dashoffset 1100ms var(--ease-out) 150ms; }
  .contact-status i { animation: breathe 3.4s ease-out infinite; }
  .toc-rail a::before { transition: width 220ms var(--ease-out), height 220ms var(--ease-out), background-color 220ms var(--ease-out); }
  .toc-rail a span { transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out); }
  /* 盖章: the chop presses onto the paper rather than fading in. Scale runs
     from slightly oversized down to rest, because that is the direction a
     real stamp travels — a grow-in reads as a popup badge. */
  .seal--idle { animation: stamp 620ms var(--ease-out) 520ms both; }
}

@keyframes stamp {
  from { opacity: 0; transform: scale(1.28) rotate(-4deg); }
  60% { opacity: 1; }
  to { opacity: 1; transform: scale(1) rotate(0deg); }
}

@keyframes breathe {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--celadon) 45%, transparent); }
  70%, 100% { box-shadow: 0 0 0 6px transparent; }
}

@keyframes soak {
  from { opacity: 0; filter: blur(7px); transform: translateY(10px); }
  to { opacity: 1; filter: blur(0); transform: translateY(0); }
}

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

/* ============================================================
   themes: 纸墨 light (base tokens above) · 科技 dark
   ("business" as a ?for= value is a retired alias for the light profile.)
   token swap + a few component overrides; applied via data-theme
   on <html> by site.js. CSP-safe: no inline styles anywhere.
   ============================================================ */

/* tech: full inversion — paper/ink swap roles, celadon pair swaps so
   text stays readable on both grounds, cinnabar lifts for dark */
[data-theme="tech"] {
  --paper: #10150f;
  --paper-raised: #151b13;
  --paper-deep: #1a2118;
  --ink: #e8ede4;
  /* deeper than --paper, so the night scroll still descends into something
     rather than opening into daylight. Deliberately NOT a palette token: it
     is the one ground on this site that is darker than 夜, which is what
     makes the foot of the scroll read as an ending. */
  --dusk: #070a06;
  --dusk-ink: #e8ede4;
  --ink-soft: #c2cabc;
  --ink-mute: #97a290;
  --line: #232b21;
  --line-dark: #364033;
  --cinnabar: #cf5138;
  --cinnabar-deep: #e0745c;
  --celadon: #52685a;
  --celadon-deep: #9fb8a4;
  /* the sheet inverts: a dark clearing in the night ground, so light
     type keeps its contrast wherever the aurora ink drifts. denser
     than the light sheet because glowing ink is brighter than wash. */
  --rice: 12, 17, 12;
  /* not fully opaque: at 1 the clearing became a flat dark panel that the
     aurora could not breathe through, which read as a card. */
  --rice-o: .94;
  --rice-lift: rgba(0, 0, 0, .34);
  /* the grain fields are mid-grey, so on a dark clearing they lighten
     rather than darken — half strength keeps it paper, not static. */
  --rice-fiber: .26;
}
[data-theme="tech"] .art-fallback { background:
  radial-gradient(42% 55% at 47% 30%, rgba(159, 184, 164, .14), rgba(159, 184, 164, 0) 70%),
  radial-gradient(30% 38% at 62% 62%, rgba(159, 184, 164, .10), rgba(159, 184, 164, 0) 70%),
  radial-gradient(16% 22% at 36% 70%, rgba(207, 81, 56, .10), rgba(207, 81, 56, 0) 70%); }
[data-theme="tech"] .head-now, [data-theme="tech"] .hero-intro { font-family: var(--font-mono); }
[data-theme="tech"] .hero-intro { font-size: .92rem; }
[data-theme="tech"] .contact p { color: color-mix(in srgb, currentColor 75%, transparent); }
[data-theme="tech"] .site-footer { color: color-mix(in srgb, var(--dusk-ink) 78%, transparent); }
[data-theme="tech"] .spec-band--paper b::before { content: "夜"; }
[data-theme="tech"] .spec-band--paper span::before { content: "night"; }
[data-theme="tech"] .spec-band--ink b::before { content: "霜"; }
[data-theme="tech"] .spec-band--ink span::before { content: "frost"; }

/* — theme switch: compact light/dark pill in the header — */
.theme-col { display: grid; align-self: center; justify-self: end; }
.theme-switch { display: flex; gap: 4px; padding: 3px; background: var(--paper-raised); border: var(--line-thin) solid var(--line); border-radius: 999px; }
.theme-button { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border: 0; border-radius: 999px; background: transparent; color: var(--ink-mute); cursor: pointer; }
.theme-button span { font-family: var(--font-cjk); font-size: .8rem; font-weight: 600; letter-spacing: .12em; }
.theme-button i { font-family: var(--font-mono); font-size: .54rem; font-style: normal; letter-spacing: .16em; text-transform: lowercase; opacity: .75; }
.theme-button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
.theme-button:hover:not([aria-pressed="true"]) { color: var(--cinnabar-deep); }
html:not([data-enhanced="true"]) .theme-col { display: none; }
