/* Pearcache — the Cabinet design, with the Gate (29–30 Sep 2026).
   One cabinet panel. Inside it, the gate: an arch holding one of the owner's
   photographs, closed on arrival with a keyhole, and on click the eight site
   marks bloom out to crown it (desktop) or the arch opens to the whole panel
   over a plain grid of marks (phones). The directory is the same cabinet
   with one tall compartment. Replaces the terminal/starfield design. See
   CLAUDE.md, "The Cabinet redesign" and "The Gate", for what was removed and
   which assets were kept. */

/* Fraunces, SIL Open Font License 1.1 (assets/fonts/Fraunces-OFL.txt). One
   variable file carries weight, optical size, SOFT and WONK. */
@font-face {
  font-family: "Fraunces";
  src: url("../fonts/fraunces-latin-full-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* Contrast ratios, computed 29–30 Sep 2026 (WCAG relative luminance).
     Text pairs clear 7:1; the non-text pairs clear 3:1. */
  --ground: #170204;
  --ink: #F7ECE6;          /* 17.29 on --ground, 13.48 on --frame, 17.74 on --niche-back */
  --ink-soft: #D9D2D0;     /* 13.45 on --ground, 13.81 on --niche-back */
  --sage: #A8C4B6;         /* 11.02 on --niche-back, 10.73 on --ground, 8.37 on --frame */
  --frame: #4A0A10;
  --line: #B5D1C4;         /* 9.60 on --frame, 12.63 on --niche-back (non-text) */
  --niche-back: #0B0103;
  --plate: #F2E4DA;        /* the arch's ring, and the name tooltips */
  --plate-ink: #2A0508;    /* 15.03 on --plate */
  --digit: #0D0203;
  --digit-ink: #B5D1C4;    /* 12.54 on --digit */
  --focus: #B5D1C4;
  --hairline: rgba(247, 236, 230, .42);  /* decorative row rules */
  /* The keyhole: a dark glyph on a warm glow. It had borrowed --niche-back for
     the glyph, which turns pale in day and left a pale hole on a pale glow. */
  --key: #0B0103;
  --glow-rim: #FFF7D6;
  --glow-core: rgba(255, 244, 200, .95);
  --glow-mid: rgba(255, 226, 150, .5);
  --glow-edge: rgba(255, 226, 150, 0);

  --font-display: "Fraunces", Georgia, serif;
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;

  --dur-fast: 160ms;
  --dur-med: 420ms;
  --dur-bloom: 520ms;
  --ease-out: cubic-bezier(.16, .9, .32, 1);

  --header-h: 44px;
  --shelf: 10px;           /* thickness of the wood between compartments */

  /* Where the bloom starts: the middle of the arch, in stage percent. */
  --gate-x: 50;
  --gate-y: 52;

  /* The arch, in stage percent, and the size of a mark. The phone block
     restates these. */
  --arch-left: 27%;
  --arch-width: 46%;
  --arch-top: 15%;
  --arch-bottom: 10%;
  --mark: 14cqh;

  /* The tint every mark wears until it is hovered: one gradient down the
     whole stage, sky to earth, fusing the desert photograph (sage scrub,
     sand, rust rock) with the site's mint and wine. Each mark samples it at
     its own height, so no two are quite the same colour and all are one. */
  --tint: linear-gradient(180deg, #8FD9B8 0%, #4F9A5E 26%, #E0A94A 50%, #C0421A 74%, #7A0A26 100%);
  --stage-ratio: 16 / 9;
}

/* Day: the colour-inverted theme, reached only by the easter egg on the pear
   mark in the header (main.js sets data-theme="day" on <html>; theme-init.js
   restores it before first paint). Night stays the default and nothing here
   follows prefers-color-scheme, on purpose: day is something you find. The
   palette is sampled from the owner's original aesthetic images. Photographs
   are not inverted, so the stage and its scrim stay dark in both themes.
   Contrast, computed 7 Oct 2026 (WCAG relative luminance); text clears 7:1,
   non-text clears 3:1. */
:root[data-theme="day"] {
  color-scheme: light;
  --ground: #E7E0E4;
  --ink: #242F3B;          /* 10.47 on --ground, 8.56 on --frame, 11.91 on --niche-back */
  --ink-soft: #283846;     /* 9.28 on --ground, 7.58 on --frame, 10.55 on --niche-back */
  --sage: #5D2460;         /* 8.58 on --ground, 7.01 on --frame, 9.75 on --niche-back */
  --frame: #D6CAD1;
  --line: #5F2A63;         /* 6.62 on --frame, 9.21 on --niche-back (non-text) */
  --niche-back: #F2EFF2;
  --plate: #F2EFF2;
  --plate-ink: #242F3B;    /* 11.91 on --plate */
  --digit: #242F3B;
  --digit-ink: #41EFD8;    /* 9.43 on --digit */
  --focus: #2526E5;        /* 5.32 on --frame, 6.51 on --ground (non-text) */
  --hairline: rgba(36, 47, 59, .42);
  /* Day keyhole: the glow turns cool, from the day palette's mint. Glyph is
     --ink, 13.0:1 against the glow's near-white core. */
  --key: #242F3B;
  --glow-rim: #E8FFFA;
  --glow-core: rgba(232, 255, 250, .96);
  --glow-mid: rgba(65, 239, 216, .5);
  --glow-edge: rgba(65, 239, 216, 0);
  --tint: linear-gradient(180deg, #41EFD8 0%, #30B2CA 26%, #AC89ED 50%, #B43FA0 74%, #723172 100%);
}

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

html { height: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  color: var(--ink);
  font: 400 1rem/1.5 var(--font-ui);
  -webkit-font-smoothing: antialiased;
}

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

a { color: var(--sage); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--ink); }

:focus { outline: none; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: 4px; }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.view-hidden { display: none !important; }

.skip-link {
  position: absolute; left: 1rem; top: -100px; z-index: 100;
  padding: .6rem 1rem; border-radius: 4px;
  background: var(--plate); color: var(--plate-ink); font-weight: 700;
}
.skip-link:focus { top: 1rem; color: var(--plate-ink); }

/* ---------- bar: solid, no blur ---------- */
.bar {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  min-height: var(--header-h);
  padding: 0 1.25rem;
}
.bar__brand {
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 44px;
  color: var(--ink); text-decoration: none;
  font: 500 1.3rem/1 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  letter-spacing: -.01em;
}
.bar__brand:hover { color: var(--ink); }
/* The easter egg: the pear mark is a button. It carries no visible
   affordance beyond the pointer; the pear turns over as the theme does. */
.bar__home { display: inline-flex; align-items: center; gap: .5rem; min-height: 44px; color: inherit; text-decoration: none; }
.bar__mode {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; margin: 0 -.5rem 0 -.6rem; padding: 0;
  background: none; border: 0; color: inherit; cursor: pointer;
}
.bar__mode img { transition: transform var(--dur-med) var(--ease-out); }
:root[data-theme="day"] .bar__mode img { transform: rotate(180deg); }
/* Colours ease across during a switch; main.js adds the class for ~400ms. */
.theme-fade, .theme-fade *, .theme-fade *::before, .theme-fade *::after {
  transition: background-color .35s, color .35s, border-color .35s, fill .35s, stroke .35s !important;
}
@media (prefers-reduced-motion: reduce) {
  .bar__mode img, :root[data-theme="day"] .bar__mode img { transition: none; }
  .theme-fade, .theme-fade *, .theme-fade *::before, .theme-fade *::after { transition: none !important; }
}
.bar nav { display: flex; gap: .25rem; font-size: .95rem; }
/* The GitHub mark matches projects.pearcache.com's bar, so the nav does not
   shift between sites. Desktop only; it fades in so it does not pop. */
.bar nav a.bar__github { justify-content: center; min-width: 44px; padding: 0; animation: pc-in .6s var(--ease-out) .25s backwards; }
.bar__github svg { display: block; width: 18px; height: 18px; }
@keyframes pc-in { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 680px) { .bar nav a.bar__github { display: none; } }
@media (prefers-reduced-motion: reduce) { .bar nav a.bar__github { animation: none; } }
/* 2.5.5-sized hit areas: every link is its own 44px-tall target. */
.bar nav a {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 .7rem;
  color: var(--ink-soft); text-decoration: none;
}
.bar nav a:hover { color: var(--ink); }
.bar nav a[aria-current="page"] {
  color: var(--ink);
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px;
  text-decoration-color: var(--line);
}

/* ---------- the cabinet ---------- */
main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

#splash-view, #directory-view, .notfound {
  flex: 1 1 auto; min-height: 0;
  display: flex;
  padding: .25rem 1.25rem .5rem;
}

/* The two views swap by display, so without this the change is a jump cut.
   Only after a navigation (main.js sets .is-routed), never on first paint,
   and opacity only. */
.is-routed #splash-view, .is-routed #directory-view { transition: opacity 200ms var(--ease-out); }
@starting-style {
  .is-routed #splash-view, .is-routed #directory-view { opacity: 0; }
}

.cabinet {
  flex: 1 1 auto; min-height: 0;
  width: 100%; max-width: 1080px; margin-inline: auto;
  display: flex; flex-direction: column;
  padding: calc(var(--shelf) + 6px);
  background: var(--frame);
  border-radius: 14px;
  /* The painted line just inside the edge. */
  box-shadow: inset 0 0 0 2px var(--frame), inset 0 0 0 4px var(--line);
}

/* Drawn from the marks: the Stem, and the open container with its solid
   node. One SVG used as a mask, so the colour is a variable. */
.cabinet__frieze {
  flex: 0 0 auto;
  height: 22px; margin-bottom: var(--shelf);
  background: var(--line);
  /* The tile repeats from the centre and clips wherever the panel ends, so
     the ends fade out rather than showing a sliver of a glyph. */
  -webkit-mask: url("../img/frieze.svg") center / auto 100% repeat-x,
    linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent) center / 100% 100% no-repeat;
  -webkit-mask-composite: source-in;
  mask: url("../img/frieze.svg") center / auto 100% repeat-x,
    linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent) center / 100% 100% no-repeat;
  mask-composite: intersect;
}

/* ---------- the stage ----------
   A 16:9 box fitted inside whatever room is left, so every position below is
   a percentage of one known shape. It is a size container, so 1cqw and 1cqh
   are 1% of its width and height, which is what lets a mark travel from the
   arch to its place with a transform alone. */
.stage-wrap {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  container-type: size;
  display: grid; place-items: center;
  border-radius: 3px;
  background: var(--niche-back);
  /* clip, not hidden: hidden makes this a scroll container, and focusing a
     control near an edge (the shuffle, a mark) then scrolls the stage off. */
  overflow: clip;
}
/* ---------- the backdrop: one of three photographs, drawn per visit ----------
   gate-init.js puts bg-forest, bg-clouds or bg-dusk on <html> before first
   paint. The photograph sits in ::before, darkened by ::after so the marks
   and the arch stay the brightest things on the stage. Motion is a slow
   scale or drift on the photo layer alone. */
.stage-wrap::before, .stage-wrap::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.stage-wrap::before { background-position: center; background-size: cover; }
.stage-wrap::after { background: linear-gradient(180deg, rgba(11, 1, 3, .55), rgba(11, 1, 3, .8)); }

/* The forest river. Breathes: a 24s scale. */
.bg-forest .stage-wrap::before { background-image: url("../img/gate/bg-forest.jpg"); background-position: 50% 40%; animation: breathe 24s ease-in-out infinite alternate; }

/* The clouds, drifting: 90s across, so slow it is only noticed later. */
.bg-clouds .stage-wrap::before { background-image: url("../img/gate/bg-clouds.jpg"); inset: -12%; animation: drift 90s linear infinite alternate; }
.bg-clouds .stage-wrap::after { background: linear-gradient(180deg, rgba(11, 1, 3, .7), rgba(11, 1, 3, .88)); }

/* The coast at dusk, pinks the frame's wine already has. Breathes. */
.bg-dusk .stage-wrap::before { background-image: url("../img/gate/bg-dusk.jpg"); background-position: 50% 30%; animation: breathe 30s ease-in-out infinite alternate; }
.bg-dusk .stage-wrap::after { background: linear-gradient(180deg, rgba(23, 2, 4, .45), rgba(23, 2, 4, .85)); }

@keyframes breathe { from { transform: scale(1); } to { transform: scale(1.06); } }
@keyframes drift { from { transform: translateX(-8%); } to { transform: translateX(8%); } }

.stage {
  position: relative;
  width: min(100cqw, 100cqh * var(--stage-ratio));
  aspect-ratio: var(--stage-ratio);
  container-type: size;
}

/* ---------- the gate ---------- */
.gate {
  position: absolute; z-index: 2;
  left: var(--arch-left); width: var(--arch-width); top: var(--arch-top); bottom: var(--arch-bottom);
  padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: 999px 999px 0 0;
  transition: transform var(--dur-bloom) var(--ease-out);
  transform-origin: 50% 100%;
}
.gate:disabled { cursor: default; }
.gate__view {
  position: absolute; inset: 0;
  border-radius: inherit;
  background: var(--niche-back) center / cover no-repeat;
  box-shadow: 0 0 0 3px var(--plate), 0 0 0 5px var(--niche-back);
  transition: opacity 220ms var(--ease-out);
}
/* The photograph in the arch: one of six per visit, and the shuffle button
   walks the pool. gate-init.js puts arch-<name> on <html>; main.js adds
   arch-swap for the 220ms the view is invisible while the picture changes.
   All six are the owner's own photographs; the three backdrops above are
   deliberately not in this pool. */
.arch-desert  .gate__view { background-image: url("../img/gate/arch-desert.jpg"); background-position: 40% 35%; }
.arch-well    .gate__view { background-image: url("../img/gate/arch-well.jpg"); }
.arch-fog     .gate__view { background-image: url("../img/gate/arch-fog.jpg"); }
.arch-blossom .gate__view { background-image: url("../img/gate/arch-blossom.jpg"); background-position: 50% 30%; }
.arch-wheel   .gate__view { background-image: url("../img/gate/arch-wheel.jpg"); background-position: 50% 40%; }
.arch-tabor   .gate__view { background-image: url("../img/gate/arch-tabor.jpg"); }
.arch-swap    .gate__view { opacity: 0; }

/* The keyhole: no words on the arch. The aura is the only thing on the site
   that loops, and only while the gate is waiting to be opened. */
.gate__key {
  position: absolute; left: 50%; top: 60%; translate: -50% -50%;
  display: grid; place-items: center;
  width: 44px; height: 44px;
  color: var(--key);
  transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out);
}
.gate__key svg { position: relative; filter: drop-shadow(0 0 3px var(--glow-rim)); }
.gate__key::before, .gate__key::after {
  content: ""; position: absolute; inset: -34px; border-radius: 50%;
  background: radial-gradient(closest-side, var(--glow-core), var(--glow-mid) 38%, var(--glow-edge) 72%);
}
.gate__key::after { animation: aura 2.6s ease-in-out infinite; }
@keyframes aura {
  0%, 100% { transform: scale(.82); opacity: .35; }
  50% { transform: scale(1.18); opacity: .9; }
}
.gate-open .gate__key { opacity: 0; transform: scale(1.6); }
.gate-open .gate__key::after { animation: none; }
.gate:active { transform: scale(.98); transition-duration: var(--dur-fast); }

/* The foot of the arch: the name plate and, on its right end once the gate
   is open, the shuffle tab. One row, so the two share a height. */
.gate__foot {
  position: absolute; z-index: 2;
  left: var(--arch-left); width: var(--arch-width); top: calc(100% - var(--arch-bottom) + .35rem);
  display: flex; justify-content: center; align-items: stretch; gap: 6px;
}
/* The plate: the frame's wine with a mint hairline, so it belongs to the
   cabinet rather than to the arch. */
.gate__plate {
  flex: 0 1 auto; min-width: 0;
  display: flex; align-items: baseline; justify-content: center; gap: .45rem;
  padding: .28rem .9rem .32rem;
  border: 1.5px solid var(--line); border-radius: 3px;
  background: var(--frame); color: var(--ink); text-decoration: none;
  transition: background-color var(--dur-fast), border-color var(--dur-fast);
}
.gate__plate:hover { background: var(--niche-back); border-color: var(--ink); color: var(--ink); }
.gate__name { font: 600 1.08rem/1.2 var(--font-display); font-variation-settings: "SOFT" 100, "WONK" 1; }
.gate__what { font-size: .78rem; color: var(--sage); white-space: nowrap; }
.gate__what::after { content: " \2192"; }

.shuffle {
  flex: 0 0 auto; min-width: 42px;
  display: grid; place-items: center;
  padding: 0; border: 1.5px solid var(--line); border-radius: 3px;
  background: var(--frame); color: var(--line); cursor: pointer;
  transition: opacity 300ms var(--ease-out) 300ms, color var(--dur-fast), background-color var(--dur-fast), border-color var(--dur-fast);
}
.shuffle svg { width: 20px; height: 20px; transition: transform 200ms var(--ease-out); }
.shuffle:hover { background: var(--niche-back); color: var(--ink); border-color: var(--ink); }
.shuffle:hover svg { transform: rotate(180deg); }
.shuffle:active svg { transform: rotate(180deg) scale(.9); transition-duration: var(--dur-fast); }
/* display, not visibility: while closed the tab must take no room, or the
   plate sits off centre. It fades in from @starting-style when it appears. */
.gate-closed .shuffle { display: none; }
@starting-style { .shuffle { opacity: 0; } }

/* ---------- the marks ---------- */
.spots { list-style: none; margin: 0; padding: 0; }

.spot {
  position: absolute; z-index: 3;
  left: calc(var(--x) * 1%); top: calc(var(--y) * 1%);
  width: var(--mark);
  translate: -50% -50%;
  transition:
    transform var(--dur-bloom) var(--ease-out),
    opacity 260ms var(--ease-out),
    visibility 0s;
}
.spot > a {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: .15rem;
  color: inherit; text-decoration: none;
  border-radius: 4px;
}
.spot__art {
  position: relative; display: block; width: 100%; aspect-ratio: 1;
  isolation: isolate;
  transition: transform 200ms var(--ease-out);
}
/* Until hover, the mark is drawn in grey and the tint layer lends it hue
   and saturation only (blend mode "color"), so its light and shadow stay
   and the colour comes from the shared gradient: a duotone of the mark. */
.spot img { width: 100%; height: 100%; filter: grayscale(1) contrast(1.05); transition: filter 220ms var(--ease-out); }
.spot__tint {
  position: absolute; inset: 0;
  mix-blend-mode: color;
  background-image: var(--tint);
  background-size: 100% 100cqh;
  background-repeat: no-repeat;
  /* Slide the stage-tall gradient so this mark shows the slice at its own
     height: the mark's top sits (--y cqh - half a mark) below the stage top. */
  background-position: 0 calc(var(--mark) / 2 - var(--y) * 1cqh);
  -webkit-mask: var(--m) center / contain no-repeat;
  mask: var(--m) center / contain no-repeat;
  transition: opacity 220ms var(--ease-out);
}
/* One mask per mark, by document order. A tenth site needs a line here. */
.spot:nth-child(1) { --m: url("../img/marks/ears.svg"); }
.spot:nth-child(2) { --m: url("../img/marks/orchard.svg"); }
.spot:nth-child(3) { --m: url("../img/marks/quail.svg"); }
.spot:nth-child(4) { --m: url("../img/marks/dragonwilds.svg"); }
.spot:nth-child(5) { --m: url("../img/marks/time.svg"); }
.spot:nth-child(6) { --m: url("../img/marks/hours.svg"); }
.spot:nth-child(7) { --m: url("../img/marks/piercings.svg"); }
.spot:nth-child(8) { --m: url("../img/marks/eggstonite.svg"); }
.spot:nth-child(9) { --m: url("../img/marks/rin.svg"); }

/* The name, as a small tooltip under the mark: no visitor should click a
   link they cannot read. Hover or focus shows it on a desktop; a touch
   screen has neither, so there it is simply always on. */
.spot__tip {
  position: absolute; left: 50%; top: calc(100% + .2rem); translate: -50% 0;
  padding: .12rem .5rem .16rem;
  background: var(--plate); color: var(--plate-ink);
  font: 600 .85rem/1.2 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 160ms var(--ease-out);
  pointer-events: none;
}
.spot > a:hover .spot__tip, .spot > a:focus-visible .spot__tip { opacity: 1; }
/* The reveal: hover or keyboard focus lifts the tint and the mark's own
   palette shows. A touch screen has no hover, so there the marks wear their
   own colours from the start rather than never showing them. */
.spot > a:hover .spot__tint, .spot > a:focus-visible .spot__tint { opacity: 0; }
.spot > a:hover img, .spot > a:focus-visible img { filter: none; }
@media (hover: none) {
  .spot__tip { opacity: 1; font-size: .78rem; }
  .spot__tint { display: none; }
  .spot img { filter: none; }
}

/* The marks stand 10cqh off the arch's outline: on a circle of radius
   50.9cqh centred at 50cqw, 55.9cqh over the crown (the arch's own circle
   is 40.9cqh), then on the line 10cqh off the legs. Every neighbour is
   23cqh from the next, centre to centre: the apex, then 26.1, 52.2 and
   78.3 degrees either side, then the leg pair at y 68.59. On the circle
   x = 50 +/- 28.66 sin(a), y = 55.9 - 50.9 cos(a); the legs are at
   x 21.37 / 78.63 (28.66cqw is 50.9cqh on a 16:9 stage).
   Places are not in document order: Dragonwilds is fourth in the list
   (it sits after Quail everywhere) but takes the apex. */
.spot:nth-child(4) { --x: 50;    --y: 5; }
.spot:nth-child(1) { --x: 37.4;  --y: 10.2; }
.spot:nth-child(2) { --x: 62.6;  --y: 10.2; }
.spot:nth-child(3) { --x: 27.37; --y: 24.72; }
.spot:nth-child(5) { --x: 72.63; --y: 24.72; }
.spot:nth-child(6) { --x: 21.96; --y: 45.62; }
.spot:nth-child(7) { --x: 78.04; --y: 45.62; }
.spot:nth-child(8) { --x: 21.37; --y: 68.59; }
.spot:nth-child(9) { --x: 78.63; --y: 68.59; }

/* Bloom order: the top of the crown first, then outward and down. */
.gate-open .spot:nth-child(4) { transition-delay: 30ms; }
.gate-open .spot:nth-child(1), .gate-open .spot:nth-child(2) { transition-delay: 60ms; }
.gate-open .spot:nth-child(3), .gate-open .spot:nth-child(5) { transition-delay: 110ms; }
.gate-open .spot:nth-child(6), .gate-open .spot:nth-child(7) { transition-delay: 160ms; }
.gate-open .spot:nth-child(8), .gate-open .spot:nth-child(9) { transition-delay: 210ms; }

/* ---------- closed ----------
   Each mark is carried back to the middle of the arch and shrunk away.
   visibility takes the links out of the tab order and the accessibility
   tree while they cannot be seen. */
.gate-closed .spot {
  transform:
    translate(calc((var(--gate-x) - var(--x)) * 1cqw), calc((var(--gate-y) - var(--y)) * 1cqh))
    scale(.35);
  opacity: 0;
  visibility: hidden;
  transition:
    transform 320ms var(--ease-out),
    opacity 200ms var(--ease-out),
    visibility 0s 320ms;
}
.gate-closed .gate { transform: scale(1.04); }

/* ---------- feedback: press always, lift only for a real pointer ---------- */
@media (hover: hover) and (pointer: fine) {
  .spot > a:hover .spot__art { transform: translateY(-3px) scale(1.04); }
  .gate-closed .gate:hover { transform: scale(1.055); }
}
.spot > a:active .spot__art { transform: scale(.96); transition-duration: var(--dur-fast); }

/* ---------- plinth: at the foot of the directory ---------- */
.plinth {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem 1.5rem;
  margin-top: var(--shelf);
  padding: .35rem .4rem .1rem;
  color: var(--ink);
}
.plinth__note { margin: 0; max-width: 46ch; font-size: .86rem; line-height: 1.45; }
.plinth__side { display: flex; align-items: center; gap: 1rem; flex: 0 0 auto; }

/* Ko-fi button. The upstream snippet's inline style="" would be dropped by
   style-src 'self', so its two declarations live here. */
.kofi-button { display: inline-flex; line-height: 0; border-radius: 3px; }
.kofi-button img { height: 32px; width: auto; border: 0; border-radius: 3px; }
.kofi-button:hover img { opacity: .88; }

.counter {
  margin: 0;
  display: flex; flex-direction: column; align-items: flex-end; gap: .2rem;
  font-size: .68rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
}
.counter-digits { display: flex; gap: 2px; letter-spacing: 0; }
.counter-digits > span[aria-hidden] {
  display: block; overflow: hidden;
  min-width: 1.05em; padding: .12em 0 .08em;
  border-radius: 2px;
  background: var(--digit); color: var(--digit-ink);
  font: 600 1.15rem/1 var(--font-display);
  font-variant-numeric: tabular-nums lining-nums;
  text-align: center;
}
/* main.js wraps each real digit in .roll; the six typed zeros have none, so
   the failure state never moves. */
.counter-digits .roll {
  display: block;
  transition: transform var(--dur-med) var(--ease-out), opacity var(--dur-med) var(--ease-out);
}
@starting-style {
  .counter-digits .roll { transform: translateY(100%); opacity: 0; }
}
.counter-digits > span:nth-child(2) .roll { transition-delay: 40ms; }
.counter-digits > span:nth-child(3) .roll { transition-delay: 80ms; }
.counter-digits > span:nth-child(4) .roll { transition-delay: 120ms; }
.counter-digits > span:nth-child(5) .roll { transition-delay: 160ms; }
.counter-digits > span:nth-child(6) .roll { transition-delay: 200ms; }

/* ---------- directory: the same cabinet, one tall compartment ---------- */
/* The directory wears the Projects page's treatment (tools-pearcache, 8 Oct
   2026), with its own photograph: the photo is blurred 6px on its own layer,
   the cabinet is a frosted pane over it, and the page inside is a second,
   denser pane. #directory-view is the window; the splash keeps its stage. */
#directory-view {
  position: relative; isolation: isolate; overflow: clip;
  align-items: flex-start; padding-block: .75rem 1rem;
}
#directory-view::before {
  content: ""; position: absolute; inset: -24px; z-index: -1;
  background:
    linear-gradient(rgba(23, 2, 4, .38), rgba(23, 2, 4, .58)),
    url("../img/gate/bg-dusk.jpg") center 30% / cover no-repeat;
  filter: blur(6px);
}
.cabinet--page { flex: 0 1 auto; max-width: 880px; }
/* Both panes fall back to the solid theme colours without backdrop-filter or
   color-mix. Text sits only on the page pane. Worst case for its contrast is
   the photograph's own extreme: pure white behind night's pane (ink 14.6,
   ink-soft 11.4, sage 9.1 on the composite) or pure black behind day's
   (ink 9.7, ink-soft 8.8, sage 8.3). */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and (background: color-mix(in srgb, red 50%, blue)) {
  .cabinet--page {
    background:
      linear-gradient(135deg, rgba(255, 255, 255, .13), rgba(255, 255, 255, 0) 45%),
      color-mix(in srgb, var(--frame) 52%, transparent);
    -webkit-backdrop-filter: blur(16px) saturate(1.15);
    backdrop-filter: blur(16px) saturate(1.15);
  }
  .cabinet--page .cabinet__page {
    background: color-mix(in srgb, var(--niche-back) 90%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .10);
  }
}
.cabinet__page {
  padding: 1.25rem 1.25rem 1.75rem;
  border-radius: 3px;
  background: var(--niche-back);
}

.dir-table { width: 100%; border-collapse: collapse; }
.dir-table caption, .hawlin-index h2 {
  text-align: left;
  margin: 0 0 .5rem;
  font: 500 1.5rem/1.2 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  letter-spacing: -.015em;
  color: var(--ink);
}
.dir-table tbody { counter-reset: dir; }
.dir-row { counter-increment: dir; border-top: 1px solid var(--hairline); }
.dir-row td { padding: .8rem .5rem; vertical-align: top; }
/* The rank number is a CSS counter, not typed into the markup: rows get
   commented out and restored, and typed numbers went stale every time. */
.dir-idx { width: 2.2em; text-align: right; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.dir-idx::before { content: counter(dir) "."; }
.dir-main a.dir-title {
  color: var(--ink); text-decoration: none;
  font: 600 1.1rem/1.3 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 0;
}
.dir-main a.dir-title:hover { text-decoration: underline; }
.dir-meta { display: block; margin-top: .2rem; font-size: .92rem; line-height: 1.7; color: var(--ink-soft); }
.dir-domain { color: var(--sage); }
.status-badge { font-size: .8rem; font-weight: 600; letter-spacing: .04em; color: var(--ink); }
.status-badge[data-status="live"]::before {
  content: ""; display: inline-block;
  width: .5em; height: .5em; margin-right: .35em; border-radius: 50%;
  background: var(--line);
}

.hawlin-index { margin-top: 2.5rem; }
.hawlin-group { margin-bottom: 1.5rem; }
.hawlin-group h3 {
  margin: 0 0 .3rem;
  font-size: .78rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-soft);
}
.hawlin-group ul { list-style: none; margin: 0; padding: 0; }
.hawlin-group li {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: .5rem 0; line-height: 1.7;
  border-top: 1px solid var(--hairline);
}
.hawlin-group li a { color: var(--ink); text-decoration: none; }
.hawlin-group li a:hover { text-decoration: underline; }
.hawlin-group li .loc { color: var(--ink-soft); white-space: nowrap; }

/* "From the archive" is HIDDEN in index.html; these rules are what makes
   uncommenting it the whole restore. */
.gallery-strip { margin-top: 2.5rem; }
.gallery-strip h2 { margin: 0 0 .35rem; font: 500 1.5rem/1.2 var(--font-display); color: var(--ink); }
.gallery-strip p.note { margin: 0 0 1rem; font-size: .95rem; color: var(--ink-soft); }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: .6rem; }
.gallery-grid figure { position: relative; margin: 0; aspect-ratio: 1; overflow: hidden; border-radius: 3px; }
.gallery-grid img { width: 100%; height: 100%; object-fit: cover; }
.gallery-grid figcaption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: .3rem .5rem;
  background: var(--plate); color: var(--plate-ink);
  font-size: .72rem;
}

/* The mini gate: the closed arch at link size, wearing the same photograph
   as the gate (gate-init.js's arch-* class), with a static keyhole. The aura
   loops only on the gate itself. */
.directory-back { margin: 2rem 0 0; display: flex; justify-content: center; }
.mini-gate {
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  min-height: 44px; padding: .25rem .5rem;
  color: var(--ink); text-decoration: none;
}
.mini-gate:hover { color: var(--ink); }
.mini-gate__arch {
  position: relative; display: block; width: 72px; height: 90px;
  border-radius: 999px 999px 0 0;
  background: var(--niche-back) center / cover no-repeat;
  box-shadow: 0 0 0 3px var(--plate), 0 0 0 5px var(--niche-back);
  transition: transform var(--dur-fast) var(--ease-out);
}
.arch-desert  .mini-gate__arch { background-image: url("../img/gate/arch-desert.jpg"); background-position: 40% 35%; }
.arch-well    .mini-gate__arch { background-image: url("../img/gate/arch-well.jpg"); }
.arch-fog     .mini-gate__arch { background-image: url("../img/gate/arch-fog.jpg"); }
.arch-blossom .mini-gate__arch { background-image: url("../img/gate/arch-blossom.jpg"); background-position: 50% 30%; }
.arch-wheel   .mini-gate__arch { background-image: url("../img/gate/arch-wheel.jpg"); background-position: 50% 40%; }
.arch-tabor   .mini-gate__arch { background-image: url("../img/gate/arch-tabor.jpg"); }
.mini-gate__key {
  position: absolute; left: 50%; top: 60%; translate: -50% -50%;
  display: grid; place-items: center; width: 44px; height: 44px;
  color: var(--key);
}
.mini-gate__key svg { position: relative; width: 11px; height: 17px; filter: drop-shadow(0 0 2px var(--glow-rim)); }
.mini-gate__key::before {
  content: ""; position: absolute; inset: -6px; border-radius: 50%;
  background: radial-gradient(closest-side, var(--glow-core), var(--glow-mid) 40%, var(--glow-edge) 72%);
}
.mini-gate__label {
  padding: .28rem .9rem .32rem;
  border: 1.5px solid var(--line); border-radius: 3px;
  background: var(--frame); color: var(--ink);
  font: 600 .95rem/1.3 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  transition: background-color var(--dur-fast), border-color var(--dur-fast);
}
.mini-gate:hover .mini-gate__label { background: var(--niche-back); border-color: var(--ink); }
.mini-gate:hover .mini-gate__arch { transform: scale(1.04); }
.mini-gate:active .mini-gate__arch { transform: scale(.97); }

.button {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 1.1rem;
  border-radius: 3px;
  background: var(--plate); color: var(--plate-ink);
  font-weight: 600; text-decoration: none;
}
.button:hover { background: var(--ink); color: var(--plate-ink); }
.button:active { transform: scale(.97); transition: transform var(--dur-fast) var(--ease-out); }

/* ---------- 404.html only ---------- */
.notfound { align-items: center; }
.notfound .cabinet { flex: 0 1 auto; max-width: 560px; }
.notfound__label {
  margin: 0 0 .3rem;
  font-size: .78rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-soft);
}
.notfound__title {
  margin: 0 0 .6rem;
  font: 500 1.6rem/1.2 var(--font-display);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  letter-spacing: -.015em;
}
.notfound p { margin: 0 0 .6rem; color: var(--ink-soft); }
.notfound__actions { margin: 1rem 0 .9rem; }

/* ---------- foot ---------- */
.site-footer {
  flex: 0 0 auto;
  padding: 0 1.25rem .45rem;
  text-align: center; font-size: .8rem; color: var(--ink-soft);
}
.site-footer p { margin: 0; }
.site-footer a { color: inherit; }

/* ---------- the non-scrolling lock: splash only, every width ----------
   dvh, not vh: on a phone, vh is the viewport with the address bar hidden,
   so a 100vh body always scrolls by the bar's height while it shows. */
body:has(#splash-view:not(.view-hidden)) { height: 100dvh; min-height: 0; overflow: hidden; }

/* ---------- phones: the gate opens all the way ----------
   Most visitors arrive here first, and the crown does not translate to a
   narrow screen. So on a phone the closed gate is the same arch with the
   keyhole, and opening it swings the arch out to the whole stage: the
   photograph becomes the background and the marks come up in a plain grid
   with their names, easy to read and to tap. Function first; the crown is
   the desktop's. */
@media (max-width: 680px) {
  :root {
    /* A wide arch, its height set by its width (about 4:5), not by the
       stage's: the stage is the whole panel here and pinning the arch to
       its top and bottom made a tall thin slot. The max() keeps it inside a
       short (landscape) stage. */
    --arch-left: 11%;
    --arch-width: 78%;
    --arch-top: max(3cqh, 50cqh - 54cqw);
    --arch-bottom: max(14cqh, 50cqh - 46cqw);
    /* 22cqw, unless three rows of marks and names would not fit in the
       82cqh the grid gets (a landscape phone): a row is 27.3cqh, of which
       the gap and name take about 1.6rem. */
    --mark: min(22cqw, 27.3cqh - 1.6rem);
    --gate-y: 50;
  }
  #splash-view, #directory-view, .notfound { padding-inline: .75rem; }
  .bar { padding-inline: .75rem; }
  .bar { gap: .5rem; }
  .bar nav a { padding: 0 .45rem; }
  /* The stage is the whole panel here, not a fixed-ratio box inside it, so
     the opened photograph reaches every edge. */
  .stage-wrap { container-type: inline-size; }
  .stage { position: absolute; inset: 0; width: auto; aspect-ratio: auto; }

  /* The arch's edges are what animate here, not a transform, so the
     photograph re-crops as it grows rather than stretching. */
  .gate {
    transition: transform var(--dur-bloom) var(--ease-out),
      left var(--dur-bloom) var(--ease-out), width var(--dur-bloom) var(--ease-out),
      top var(--dur-bloom) var(--ease-out), bottom var(--dur-bloom) var(--ease-out),
      border-radius var(--dur-bloom) var(--ease-out);
  }
  .gate-open .gate { left: 0; width: 100%; top: 0; bottom: 0; border-radius: 3px; }
  .gate-open .gate__view { box-shadow: none; }
  /* A scrim over the open photograph so the marks and names read on it. */
  .gate::after {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    background: linear-gradient(180deg, rgba(11, 1, 3, .45), rgba(11, 1, 3, .7));
    opacity: 0; transition: opacity var(--dur-bloom) var(--ease-out);
  }
  .gate-open .gate::after { opacity: 1; }

  /* The marks: a three-by-three grid over the photograph, in document
     order, names always on. Nine marks fill it, so none stands alone. */
  .spots {
    position: absolute; z-index: 3; inset: 3% 4% 15%;
    display: grid; grid-template-columns: repeat(3, 1fr); place-items: center;
    pointer-events: none; /* the list covers the closed gate; only the links take taps */
  }
  .spot > a { pointer-events: auto; }
  .spot, .gate-closed .spot { position: static; translate: none; }
  .spot { transition: transform 360ms var(--ease-out), opacity 240ms var(--ease-out), visibility 0s; }
  .spot > a { gap: .3rem; }
  .spot__tip { position: static; translate: none; }
  /* Each mark grows in where it stands, a beat after the arch has opened. */
  .gate-closed .spot { transform: scale(.6); transition: transform 240ms var(--ease-out), opacity 200ms var(--ease-out), visibility 0s 240ms; }
  .gate-open .spot:nth-child(-n + 9) { transition-delay: 320ms; }
  .gate-open .spot:nth-child(n + 4) { transition-delay: 380ms; }
  .gate-open .spot:nth-child(n + 7) { transition-delay: 440ms; }

  .gate__plate { flex-wrap: wrap; gap: 0 .5rem; }
  .gate__foot { transition: left var(--dur-bloom) var(--ease-out), width var(--dur-bloom) var(--ease-out), top var(--dur-bloom) var(--ease-out), translate var(--dur-bloom) var(--ease-out); }
  .gate-open .gate__foot { left: 6%; width: 88%; top: 97%; translate: 0 -100%; }

  .plinth { flex-direction: column; align-items: flex-start; }
  .plinth__side { width: 100%; justify-content: space-between; }
  .cabinet__page { padding: 1rem .75rem 1.5rem; }
  .hawlin-group li { flex-direction: column; gap: 0; }
}
/* "Projects" is three letters longer than the old "Tools". At 320px the
   brand, a flex item, shrank under its own text and ran into "Home"; it no
   longer shrinks, and the nav gives up the room. The Projects site
   (tools-pearcache) carries the same step. */
@media (max-width: 380px) {
  .bar { gap: .25rem; }
  .bar__brand { flex-shrink: 0; font-size: 1.05rem; gap: .35rem; }
  .bar nav { font-size: .84rem; gap: 0; }
  .bar nav a { padding: 0 .3rem; }
}

/* Short desktop windows: give the height back to the stage. */
@media (min-width: 681px) and (max-height: 640px) {
  .cabinet__frieze { height: 16px; }
}

/* ---------- reduced motion: the marks fade in where they stand ---------- */
@media (prefers-reduced-motion: reduce) {
  .stage-wrap::before { animation: none; }
  .spot, .gate-closed .spot { transform: none; transition: opacity 200ms ease, visibility 0s; transition-delay: 0ms !important; }
  .gate-closed .spot { transition: opacity 200ms ease, visibility 0s 200ms; }
  .gate, .gate-closed .gate, .gate-closed .gate:hover { transform: none; transition: none; }
  .spot__art { transition: none; transform: none !important; }
  .gate__key::after { animation: none; opacity: .7; }
  .gate__key { transition: opacity 200ms ease; }
  .gate-open .gate__key { transform: none; }
  .button { transition: none; transform: none !important; }
  .mini-gate__arch, .mini-gate:hover .mini-gate__arch, .mini-gate:active .mini-gate__arch { transition: none; transform: none; }
  .counter-digits .roll { transition: opacity 200ms ease; transition-delay: 0ms !important; }
  @starting-style {
    .counter-digits .roll { transform: none; }
  }
}
