/* ==========================================================================
   Wynn Fu — Portfolio 2026
   A horizontal reading experience. Chrome adapts to the page in view.
   ========================================================================== */

/* --- tokens --------------------------------------------------------------- */

:root {
  --ratio: 1.41455;          /* the portfolio's page proportion */

  --ink: #08090a;
  --accent: #73e56b;         /* brand green, sampled from the cover */
  --ambient: #73e56b;        /* live-tinted by the page in view */
  --page-avg: #08090a;

  /* Filmstrip metrics live here so the dock's height, the help button's
     position and the rail's own layout maths all derive from one source. */
  --thumb-w: 58px;
  --thumb-h: 41px;
  --thumb-active-w: 76px;
  --thumb-active-h: 54px;
  --thumb-gap: 5px;
  --thumb-group-extra: 9px;
  --strip-pad: 7px;
  --strip-inset: 7px;
  --film-pad: 7px;
  /* The help button matches the dock's own toggle, so the two read as a pair. */
  --toggle-size: 38px;
  --help-size: var(--toggle-size);

  --strip-h: calc(var(--thumb-active-h) + 2 * var(--strip-pad) + 2 * var(--film-pad) + 2px);
  --dock-pad: max(14px, env(safe-area-inset-bottom));
  --dock-h: calc(var(--strip-h) + var(--dock-pad) + 10px);

  --plate-h: min(64dvh, calc(86vw / var(--ratio)));
  --plate-w: calc(var(--plate-h) * var(--ratio));
  --gap: clamp(28px, 3.6vw, 68px);
  --edge: max(20px, 3.4vw);

  --font-ui: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Inter',
             system-ui, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'SF Mono', ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, monospace;
  --font-script: 'Snell Roundhand', 'Pinyon Script', 'Apple Chancery', 'Segoe Script', cursive;

  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.32, .72, 0, 1);

  --radius-pill: 999px;
  --radius-card: 18px;

  /* The page caption sits on the room wash rather than on glass. The wash stays
     dark even where the artwork is bright enough to turn the chrome light, so
     these live outside the two chrome blocks: they must not flip. */
  --cap-fg: rgba(247, 245, 243, .9);
  --cap-title: rgba(255, 255, 255, .95);
  --cap-index: var(--accent);

  --z-stage: 1;
  --z-dock: 40;
  --z-top: 50;
  --z-sheet: 60;
  --z-lightbox: 80;
  --z-loader: 90;
}

/* Glass, dark surface: translucent, blurred, with a specular top edge. */
:root[data-chrome='dark'] {
  --ui-fg: rgba(255, 255, 255, .94);
  --ui-fg-dim: rgba(255, 255, 255, .58);
  --ui-fg-faint: rgba(255, 255, 255, .34);
  --accent-ui: var(--accent);
  --accent-glow: 0 0 22px color-mix(in oklab, var(--accent) 45%, transparent);
  --glass-tint: linear-gradient(180deg, rgba(255, 255, 255, .13), rgba(255, 255, 255, .055));
  --glass-line: rgba(255, 255, 255, .16);
  --glass-spec: rgba(255, 255, 255, .32);
  --glass-shadow: 0 22px 60px -18px rgba(0, 0, 0, .7), 0 3px 12px rgba(0, 0, 0, .3);
  --glass-hover: rgba(255, 255, 255, .1);
  --glass-active: linear-gradient(180deg, rgba(255, 255, 255, .22), rgba(255, 255, 255, .11));
  --plate-ring: rgba(255, 255, 255, .1);
  --plate-shadow: 0 40px 90px -30px rgba(0, 0, 0, .85), 0 6px 24px rgba(0, 0, 0, .4);
}

/* Glass, light surface: for the kraft and paper-white pages. */
:root[data-chrome='light'] {
  --ui-fg: rgba(14, 14, 16, .92);
  --ui-fg-dim: rgba(14, 14, 16, .62);
  --ui-fg-faint: rgba(14, 14, 16, .38);
  /* A chapter accent tuned for a pale surface keeps its hue but earns contrast. */
  --accent-ui: color-mix(in oklab, var(--accent) 60%, #17111c);
  --accent-glow: none;
  --glass-tint: linear-gradient(180deg, rgba(255, 255, 255, .72), rgba(255, 255, 255, .5));
  --glass-line: rgba(255, 255, 255, .8);
  --glass-spec: rgba(255, 255, 255, .95);
  --glass-shadow: 0 22px 60px -20px rgba(24, 16, 12, .34), 0 2px 10px rgba(24, 16, 12, .12);
  --glass-hover: rgba(0, 0, 0, .05);
  --glass-active: linear-gradient(180deg, rgba(255, 255, 255, .96), rgba(255, 255, 255, .82));
  --plate-ring: rgba(0, 0, 0, .08);
  --plate-shadow: 0 40px 90px -34px rgba(30, 20, 14, .5), 0 6px 20px rgba(30, 20, 14, .16);
}

/* --- base ---------------------------------------------------------------- */

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

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;               /* the stage owns the scrolling */
  overscroll-behavior: none;
}

body {
  /* --room is re-tinted from the page in view, so it needs no transition. */
  background-color: var(--room, var(--ink));
  color: var(--ui-fg);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  transition: color .7s var(--ease-soft);
}

img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul { margin: 0; padding: 0; list-style: none; }

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

.skiplink {
  position: fixed; top: 12px; left: 50%; z-index: 200;
  transform: translate(-50%, -160%);
  padding: 10px 18px; border-radius: var(--radius-pill);
  background: var(--accent); color: #06210a;
  font-size: 12px; font-weight: 600; letter-spacing: .04em;
  transition: transform .3s var(--ease-out);
}
.skiplink:focus-visible { transform: translate(-50%, 0); }

:focus-visible {
  outline: 2px solid var(--accent-ui);
  outline-offset: 3px;
  border-radius: 6px;
}

/* --- ambient backdrop ---------------------------------------------------- */

.ambient { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }

/* Light spills from above and below the page rather than from one side, so the
   room reads as a lit wall instead of a spotlight. */
.ambient__glow--a {
  position: absolute; inset: -10%;
  background:
    radial-gradient(84% 62% at 50% 112%, color-mix(in oklab, var(--ambient) 70%, transparent) 0%, transparent 68%),
    radial-gradient(96% 52% at 50% -12%, color-mix(in oklab, var(--ambient) 44%, transparent) 0%, transparent 70%);
  filter: blur(36px);
  opacity: .13;
}
/* A slow wandering blob keeps the wash from looking printed on. */
.ambient__glow--b {
  position: absolute; inset: -30%;
  background: radial-gradient(38% 44% at 50% 50%,
              color-mix(in oklab, var(--ambient) 80%, transparent) 0%, transparent 70%);
  filter: blur(60px);
  opacity: .07;
  animation: drift 42s ease-in-out infinite alternate;
}
@keyframes drift {
  from { transform: translate3d(-16%, 6%, 0) scale(1); }
  to   { transform: translate3d(16%, -6%, 0) scale(1.25); }
}

.ambient__vignette {
  position: absolute; inset: 0;
  background:
    radial-gradient(120% 90% at 50% 50%, transparent 42%, rgba(0, 0, 0, .5) 100%),
    linear-gradient(180deg, rgba(0, 0, 0, .34) 0%, transparent 22%, transparent 70%, rgba(0, 0, 0, .46) 100%);
}

/* Fine grain keeps the large flat areas from banding. */
.ambient__grain {
  position: absolute; inset: -50%;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain 7s steps(6) infinite;
}
@keyframes grain {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-4%, 2%); }
  40% { transform: translate(3%, -3%); }
  60% { transform: translate(-2%, -2%); }
  80% { transform: translate(2%, 3%); }
}

/* --- glass surface ------------------------------------------------------- */

.glass {
  position: relative;
  background-image: var(--glass-tint);
  background-color: color-mix(in srgb, var(--page-avg) 26%, transparent);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-pill);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-spec), inset 0 -1px 0 rgba(255, 255, 255, .06);
  -webkit-backdrop-filter: blur(26px) saturate(180%);
  backdrop-filter: blur(26px) saturate(180%);
  transition:
    background-color .8s var(--ease-soft),
    background-image .8s var(--ease-soft),
    border-color .8s var(--ease-soft),
    box-shadow .8s var(--ease-soft),
    opacity .5s var(--ease-out),
    transform .5s var(--ease-out);
}

/* Specular sheen that follows the pointer — the 'liquid' in liquid glass. */
.glass::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(120px 80px at var(--mx, 50%) var(--my, 0%),
              rgba(255, 255, 255, .3), transparent 72%);
  transition: opacity .45s var(--ease-out);
  mix-blend-mode: soft-light;
}
.glass:hover::after { opacity: 1; }

/* --- top bar ------------------------------------------------------------- */

.topbar {
  position: fixed; z-index: var(--z-top);
  top: max(18px, 2vh); left: var(--edge); right: var(--edge);
  display: flex; align-items: center; gap: 12px;
  pointer-events: none;
}
.topbar > * { pointer-events: auto; }

.brand {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 20px 9px 15px;
  flex: none;
}
.brand__mark {
  font-family: var(--font-script);
  font-size: 27px; line-height: 1;
  letter-spacing: .01em;
  color: var(--accent-ui);
  transform: translateY(-2px);
  text-shadow: var(--accent-glow);
  transition: color .7s var(--ease-soft), text-shadow .7s var(--ease-soft);
}
.brand__text { display: flex; flex-direction: column; gap: 1px; }
.brand__name { font-size: 13px; font-weight: 590; letter-spacing: -.005em; }
.brand__role {
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ui-fg-dim);
}

/* nav */
.navbar { margin-left: auto; padding: 5px; flex: none; }

.navbar__list { display: flex; align-items: center; gap: 1px; position: relative; z-index: 1; }

.navbar__item > a {
  display: block;
  padding: 8px 13px;
  border-radius: var(--radius-pill);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: .015em;
  color: var(--ui-fg-dim);
  white-space: nowrap;
  transition: color .35s var(--ease-out);
}
.navbar__item > a:hover { color: var(--ui-fg); }
.navbar__item[aria-current='true'] > a { color: var(--ui-fg); font-weight: 590; }

.navbar__item--rule {
  width: 1px; height: 15px; margin: 0 7px;
  background: currentColor; opacity: .16;
}

/* The pill slides between chapters instead of snapping. */
.navbar__pill {
  position: absolute; top: 0; left: 0; z-index: 0;
  border-radius: var(--radius-pill);
  background-image: var(--glass-active);
  box-shadow: inset 0 1px 0 var(--glass-spec), 0 2px 8px rgba(0, 0, 0, .18);
  opacity: 0;
  transform: translate3d(0, 0, 0);
  transition:
    transform .58s var(--ease-out),
    width .58s var(--ease-out),
    opacity .4s var(--ease-out),
    background-image .8s var(--ease-soft);
}
.navbar__pill.is-on { opacity: 1; }

/* Accent dot marks the chapter you are inside. */
.navbar__item[aria-current='true'] > a::before {
  content: '';
  position: absolute;
  margin-top: 17px; margin-left: -5px;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--accent-ui);
}

/* compact menu button, shown on narrow screens */
.navmenu {
  display: none;
  margin-left: auto;
  align-items: center; gap: 9px;
  padding: 10px 15px;
  font-size: 11.5px; font-weight: 550;
}
.navmenu__icon { width: 10px; transition: transform .4s var(--ease-out); }
.navmenu[aria-expanded='true'] .navmenu__icon { transform: rotate(180deg); }

/* --- stage & plates ------------------------------------------------------ */

.stage {
  position: fixed; inset: 0;
  z-index: var(--z-stage);
  overflow: hidden;
  cursor: grab;
}
.stage.is-dragging { cursor: grabbing; }

.track {
  display: flex;
  align-items: center;
  /* The bar and the dock frame the artwork, and the dock is the taller of the
     two, so centring on the raw window would leave more air above than below.
     Discounting the difference puts the page on the optical centre of the band
     they enclose, which is where the eye expects the middle to be. */
  height: 100%;
  padding-bottom: max(0px, calc(var(--dock-h) - var(--bar-band, 62px)));
  width: max-content;
  /* The lead-in centres the cover; the closing panel is its own runway, so the
     track needs no matching space at the end. */
  padding-left: max(var(--edge), calc(50vw - var(--plate-w) / 2));
  padding-right: var(--edge);
  gap: var(--gap);
  will-change: transform;
  transform: translate3d(0, 0, 0);
  outline: none;
}

.plate {
  position: relative;
  flex: none;
  width: var(--plate-w);
  height: var(--plate-h);
  margin: 0;
}

.plate__hit {
  display: block;
  width: 100%; height: 100%;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--page-avg);
  box-shadow: var(--plate-shadow), inset 0 0 0 1px var(--plate-ring);
  transform: translate3d(0, 0, 0) scale(.985);
  opacity: .55;
  filter: saturate(.86);
  transition:
    transform .8s var(--ease-soft),
    opacity .8s var(--ease-soft),
    filter .8s var(--ease-soft),
    box-shadow .8s var(--ease-soft);
  cursor: zoom-in;
}
/* The plate at centre stage comes fully forward. */
.plate.is-active .plate__hit { transform: translate3d(0, 0, 0) scale(1); opacity: 1; filter: none; }
.plate.is-near .plate__hit { opacity: .82; filter: saturate(.94); }

.plate__img {
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .7s var(--ease-out);
}
.plate__img.is-loaded { opacity: 1; }

/* Low-quality placeholder holds the page's colour until the real file lands. */
.plate__lqip {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: blur(14px) saturate(1.1);
  transform: scale(1.06);
  transition: opacity .6s var(--ease-out);
}
.plate.is-loaded .plate__lqip { opacity: 0; }

.plate__num {
  position: absolute; top: 14px; left: 16px;
  font-family: var(--font-mono);
  font-size: 9px; letter-spacing: .16em;
  padding: 5px 9px;
  border-radius: var(--radius-pill);
  color: #fff;
  background: rgba(0, 0, 0, .42);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity .4s var(--ease-out);
  pointer-events: none;
}
.plate:hover .plate__num { opacity: .9; }

.plate__cap {
  position: absolute; left: 2px; right: 2px; top: calc(100% + 15px);
  display: flex; align-items: baseline; gap: 10px;
  font-size: 11.5px;
  color: var(--cap-fg);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity .6s var(--ease-out), transform .6s var(--ease-out), color .7s var(--ease-soft);
}
.plate.is-active .plate__cap { opacity: 1; transform: none; }
.plate__cap b {
  font-family: var(--font-mono);
  font-size: 9px; font-weight: 400; letter-spacing: .16em;
  color: var(--cap-index);
  flex: none;
}
.plate__cap span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.plate__zoom {
  position: absolute; right: 14px; bottom: 14px;
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border-radius: 50%;
  color: #fff;
  background: rgba(0, 0, 0, .4);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  transform: scale(.85);
  transition: opacity .35s var(--ease-out), transform .35s var(--ease-out);
  pointer-events: none;
}
.plate:hover .plate__zoom { opacity: 1; transform: none; }
.plate__zoom svg { width: 13px; }

/* --- chapter dividers ---------------------------------------------------- */

.divider {
  flex: none;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 20px;
  width: clamp(200px, 20vw, 300px);
  height: var(--plate-h);
  padding: 0 clamp(8px, 1.6vw, 28px);
  text-align: center;
  opacity: .45;
  transition: opacity .8s var(--ease-soft);
}
.divider.is-near { opacity: 1; }

.divider__rule {
  flex: 1;
  width: 1px;
  min-height: 24px;
  background: linear-gradient(180deg, transparent, var(--ui-fg-faint) 45%, var(--ui-fg-faint) 55%, transparent);
}
.divider__index {
  font-family: var(--font-mono);
  font-size: 9.5px; letter-spacing: .3em;
  color: var(--accent-ui);
}
.divider__title {
  font-family: var(--font-script);
  font-size: clamp(30px, 3.6vw, 50px);
  line-height: 1.06;
  font-weight: 400;
  margin: 0;
  color: var(--ui-fg);
  transition: color .7s var(--ease-soft);
}
.divider__marks {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .34em;
  color: var(--ui-fg-faint);
}
.divider__meta {
  display: flex; flex-direction: column; gap: 5px;
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ui-fg-dim);
}
.divider__link {
  font-family: var(--font-mono);
  font-size: 9px; letter-spacing: .12em; text-transform: uppercase;
  padding: 7px 13px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--glass-line);
  color: var(--ui-fg);
  transition: background-color .3s var(--ease-out), border-color .3s var(--ease-out);
}
.divider__link:hover { background: var(--glass-hover); }

/* The closing panel: contact details as real, selectable text. The panel is cut
   to the height of the artwork beside it, and it now carries a colophon as well
   as the invitation, so the spacing gives way in a short window rather than
   letting the column grow past the page it stands next to. */
.outro {
  flex: none;
  display: flex; flex-direction: column; justify-content: center;
  /* Full rhythm in a tall window, and a ramp that has closed it to its tightest
     by the time the window is around 700 high — the colophon added four lines to
     a column that is already the tallest thing on the spread, and the spacing is
     what should give way rather than the bar above or the dock below. */
  gap: clamp(10px, calc(11vh - 73px), 26px);
  width: clamp(320px, 34vw, 480px);
  /* Cut to the artwork's height, but free to take more if the words need it: the
     track centres whatever height it is given, and there is air to spare above
     and below in the windows where the column runs long. */
  min-height: var(--plate-h);
  padding-inline: clamp(16px, 3vw, 44px);
}
.outro__kicker {
  font-family: var(--font-mono);
  font-size: 9px; letter-spacing: .3em; text-transform: uppercase;
  color: var(--accent-ui);
}
.outro__title {
  font-family: var(--font-script);
  font-size: clamp(38px, 5vw, 66px);
  line-height: 1;
  margin: 0;
  font-weight: 400;
}
.outro__lead { margin: 0; max-width: 30ch; color: var(--ui-fg-dim); font-size: 14px; }
.outro__links { display: flex; flex-direction: column; gap: 10px; }
.outro__link {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 18px;
  border-radius: 14px;
  border: 1px solid var(--glass-line);
  background-image: var(--glass-tint);
  transition: background-color .35s var(--ease-out), transform .35s var(--ease-out);
}
.outro__link:hover { transform: translateX(4px); background-color: var(--glass-hover); }
.outro__link b {
  font-family: var(--font-mono);
  font-size: 8.5px; font-weight: 400; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ui-fg-faint);
  width: 62px; flex: none;
}
.outro__link span { font-size: 14px; }
/* The colophon: the imprint of the work, and the credit for the site that
   carries it. A rule sets both below the invitation, and the credit borrows the
   labelling the contact rows use above it — the mono capitals for what it is,
   then the name, then the addresses, each a step quieter than the last. */
.outro__colophon {
  display: flex; flex-direction: column; gap: clamp(11px, 2.1vh, 19px);
  padding-top: clamp(14px, 2.5vh, 22px);
  border-top: 1px solid var(--glass-line);
}
.outro__foot {
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ui-fg-faint);
}
.outro__credit { display: flex; flex-direction: column; gap: 3px; }
/* A heading for the three lines under it, so it stands a little further off. */
.outro__credit-thanks { margin-bottom: 4px; }
.outro__credit-role {
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ui-fg-faint);
}
.outro__credit-who { margin: 0; font-size: 11.5px; color: var(--ui-fg-dim); }
.outro__credit-who b { font-weight: 560; color: var(--ui-fg); }
.outro__credit-reach {
  margin: 0;
  font-size: 11px; line-height: 1.75;
  color: var(--ui-fg-faint);
}
.outro__credit-reach a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 32%, transparent);
  text-underline-offset: 3px;
  transition: color .3s var(--ease-out), text-decoration-color .3s var(--ease-out);
}
.outro__credit-reach a:hover {
  color: var(--accent-ui);
  text-decoration-color: currentColor;
}

/* --- scroll hint --------------------------------------------------------- */

.hint {
  position: fixed; z-index: 30;
  left: 50%; bottom: calc(var(--dock-h) + 6px);
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 12px;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 9px; letter-spacing: .26em; text-transform: uppercase;
  color: var(--ui-fg-faint);
  pointer-events: none;
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out);
}
.hint.is-gone { opacity: 0; transform: translate(-50%, 8px); }
.hint__rule {
  display: block; width: 54px; height: 1px;
  background: linear-gradient(90deg, var(--ui-fg-faint), transparent);
  animation: hintPulse 2.6s var(--ease-soft) infinite;
}
@keyframes hintPulse {
  0%, 100% { transform: translateX(0); opacity: .5; }
  50% { transform: translateX(9px); opacity: 1; }
}

/* --- dock ---------------------------------------------------------------- */

.dock {
  position: fixed; z-index: var(--z-dock);
  left: 0; right: 0; bottom: 0;
  padding: 0 var(--edge) var(--dock-pad);
  pointer-events: none;
}
/* The read-out and the toggle take exactly what they need and the filmstrip
   takes the rest, so the strip is the only thing that gives when the window
   narrows. The read-out's width is fixed, so the strip still never shifts as
   the caption changes length. */
.dock__inner {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: end;
  gap: 12px;
}
.dock__inner > * { pointer-events: auto; }

/* live read-out of where you are */
.dock__readout {
  justify-self: start;
  display: flex; align-items: center; gap: 13px;
  padding: 9px 16px;
  width: clamp(190px, 21vw, 280px);
  border-radius: 16px;
}
.readout__index {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .12em;
  color: var(--accent-ui);
  padding-right: 13px;
  border-right: 1px solid var(--glass-line);
  transition: color .7s var(--ease-soft);
}
/* Takes the slack so the page count stays pinned to the right edge instead of
   sliding about with the length of the title. */
.readout__body { flex: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.readout__title {
  font-size: 12px; font-weight: 590; letter-spacing: -.005em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.readout__caption {
  font-size: 10.5px; color: var(--ui-fg-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Signed on the read-out's top edge the way a name is signed on a line: the
   letters stand in the gap up to the filmstrip, and the tail of the y drops
   below the edge and across the glass.

   The scan carries the proportions. Of its 774px height, the 300 below the
   writing line belong to that tail, so the 474 above it are what has to cover
   the climb from this edge to the filmstrip's — and the whole mark follows from
   that one measurement, at any size the dock takes. */
.readout__sign {
  /* The strip is as tall as the dock's row, and the read-out is set at its
     foot; 100% is this box's padding, hence the glass border on either side. */
  --climb: calc(var(--strip-h) - 100% - 2px);
  position: absolute;
  right: 32px;
  top: calc(-1 * var(--climb) - 1px);
  height: calc(var(--climb) * 774 / 474);
  aspect-ratio: 1476 / 774;
  pointer-events: none;
  /* Ink, not image: the mask is painted, so the mark answers to the chapter
     colour exactly as the number beside it does. */
  background-color: var(--accent-ui);
  transition: background-color .7s var(--ease-soft);
  -webkit-mask: url(../signature.png) no-repeat center / 100% 100%;
  mask: url(../signature.png) no-repeat center / 100% 100%;
}

.readout__count {
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .06em;
  color: var(--ui-fg-faint);
  flex: none;
}
.readout__count b { font-weight: 400; color: var(--ui-fg); }
.readout__count i { font-style: normal; padding: 0 2px; }

/* filmstrip */
.filmstrip {
  display: flex; align-items: center; gap: 2px;
  padding: var(--film-pad) 8px;
  /* Fills its track up to a comfortable maximum, and gives ground rather than
     pushing the toggle past the edge of the window. */
  justify-self: center;
  width: 100%; min-width: 0;
  max-width: min(72vw, 980px);
  border-radius: 20px;
  transform-origin: bottom center;
}
.dock.is-collapsed .filmstrip {
  opacity: 0;
  transform: translateY(14px) scale(.96);
  pointer-events: none;
}
.dock.is-idle .filmstrip { opacity: .42; }
.dock.is-idle .filmstrip:hover { opacity: 1; }

.strip__arrow {
  flex: none;
  display: grid; place-items: center;
  width: 26px; height: 44px;
  margin: 0 3px;
  border-radius: 12px;
  color: var(--ui-fg-dim);
  transition: color .3s var(--ease-out), background-color .3s var(--ease-out), opacity .3s;
}
.strip__arrow svg { width: 6px; }
.strip__arrow:hover { color: var(--ui-fg); background: var(--glass-hover); }
.strip__arrow[disabled] { opacity: .22; pointer-events: none; }

/* No horizontal padding, so clientWidth is exactly the visible rail width and
   the centring maths needs no correction. */
.strip__viewport { overflow: hidden; min-width: 0; padding: var(--strip-pad) 0; }
.strip__rail {
  display: flex; align-items: center; gap: var(--thumb-gap);
  /* Breathing room at both ends: when the rail parks against either end the
     focused thumbnail's ring still sits inside the bar. */
  padding-inline: var(--strip-inset);
  /* max-content makes the rail as wide as its thumbnails, so its measured width
     is the real content width rather than the clipped viewport's. */
  width: max-content;
  transition: transform .55s var(--ease-out);
  will-change: transform;
}

.strip__group { display: flex; align-items: center; gap: var(--thumb-gap); }
.strip__group + .strip__group { margin-left: var(--thumb-group-extra); position: relative; }
.strip__group + .strip__group::before {
  content: '';
  position: absolute;
  left: calc((var(--thumb-gap) + var(--thumb-group-extra)) / -2);
  top: 22%; bottom: 22%;
  width: 1px; background: var(--glass-line);
}

.thumb {
  position: relative;
  flex: none;
  width: var(--thumb-w); height: var(--thumb-h);
  border-radius: 6px;
  overflow: hidden;
  background: var(--page-avg);
  opacity: .5;
  filter: saturate(.7) brightness(.85);
  transition:
    opacity .45s var(--ease-out),
    filter .45s var(--ease-out),
    transform .45s var(--ease-out),
    width .55s var(--ease-out),
    height .55s var(--ease-out),
    box-shadow .45s var(--ease-out);
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }

/* Only the unfocused thumbnails lift on hover; the focused one is already
   forward, and the rail's padding leaves room for the lift to stay clear of
   the strip's top edge. */
.thumb:not(.is-active):hover {
  opacity: .92;
  filter: none;
  transform: translate3d(0, -3px, 0);
}

/* The focused page takes a genuinely larger box, so its neighbours are pushed
   apart rather than covered. */
.thumb.is-active {
  opacity: 1;
  filter: none;
  z-index: 2;
  width: var(--thumb-active-w); height: var(--thumb-active-h);
  box-shadow: 0 0 0 1.5px var(--accent-ui), 0 10px 22px -8px rgba(0, 0, 0, .7);
}
.thumb.is-active::after {
  content: '';
  position: absolute; inset: 0;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .3);
  border-radius: inherit;
}

.dock__toggle {
  justify-self: end;
  display: grid; place-items: center;
  width: var(--toggle-size); height: var(--toggle-size);
  border-radius: 13px;
  color: var(--ui-fg-dim);
}
.dock__toggle svg { width: 12px; }
.dock__toggle:hover { color: var(--ui-fg); }

/* Progress rail. It runs the full width of the window, border to border, while
   the marks along it are inset to the width of the dock's row above — see
   filmstrip.js, which measures the two against each other.

   Like the page caption, the rail lies on the room wash, which stays dark under
   even the brightest page — so its colours are fixed rather than taken from the
   chrome, which would turn them dark on dark for the light chapters. */
.progress {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px;
  background: rgba(255, 255, 255, .1);
}
.progress__fill {
  position: absolute; inset: 0 auto 0 0;
  width: 0;
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--accent) 55%, transparent),
    var(--accent));
}
.progress__ticks { position: absolute; inset: 0; }
.progress__ticks i {
  position: absolute; top: 0; bottom: 0;
  width: 2px;
  margin-left: -1px;  /* centred on its chapter, so the fill's edge meets it */
  border-radius: 1px;
  background: rgba(255, 255, 255, .4);
}

/* --- mobile chapter sheet ------------------------------------------------ */

.sheet {
  /* One measurement for where it hangs from, so the room left below it can be
     worked out from the same number. */
  --sheet-top: calc(max(18px, 2vh) + 52px);
  position: fixed; z-index: var(--z-sheet);
  top: var(--sheet-top); right: var(--edge);
  width: min(280px, calc(100vw - 2 * var(--edge)));
  /* Ten chapters are more than a shallow window can hold, and a menu that runs
     off the bottom of the screen hides the very rows it was opened for. So the
     sheet stops at the foot of the window and carries the rest by scrolling. */
  max-height: calc(100dvh - var(--sheet-top) - max(14px, 2vh));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  border-radius: 20px;
  background-image: var(--glass-tint);
  background-color: color-mix(in srgb, var(--page-avg) 40%, transparent);
  border: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-spec);
  -webkit-backdrop-filter: blur(28px) saturate(180%);
  backdrop-filter: blur(28px) saturate(180%);
  transform-origin: top right;
  animation: sheetIn .42s var(--ease-out);
}
@keyframes sheetIn {
  from { opacity: 0; transform: translateY(-8px) scale(.96); }
}
.sheet__list > li > a {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px;
  border-radius: 13px;
  font-size: 13.5px;
  transition: background-color .3s var(--ease-out);
}
.sheet__list > li > a:hover { background: var(--glass-hover); }
.sheet__list > li[aria-current='true'] > a { background-image: var(--glass-active); font-weight: 590; }
.sheet__list em {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 9px; letter-spacing: .14em;
  color: var(--ui-fg-faint);
}
.sheet__rule { height: 1px; margin: 6px 10px; background: var(--glass-line); }

/* --- lightbox ------------------------------------------------------------ */

.lightbox {
  position: fixed; inset: 0; z-index: var(--z-lightbox);
  display: grid; place-items: center;
  padding: clamp(16px, 4vh, 56px);
  background: rgba(4, 4, 5, .82);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  animation: fade .4s var(--ease-out);
}
.lightbox[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } }

/* Absolute inset gives the image a definite box to be contained by, which a
   percentage max-height inside a grid does not reliably provide. */
.lightbox__stage {
  position: absolute;
  /* Extra room at the foot keeps the artwork clear of the controls. */
  inset: clamp(16px, 4vh, 56px) clamp(16px, 4vh, 56px) calc(max(20px, 3vh) + 76px);
  overflow: hidden;
}
.lightbox__img {
  width: 100%; height: 100%;
  object-fit: contain;
  /* drop-shadow follows the contained artwork; box-shadow would trace the box. */
  filter: drop-shadow(0 40px 90px rgba(0, 0, 0, .75));
  transform-origin: center;
  transition: transform .6s var(--ease-out), opacity .3s var(--ease-out);
  cursor: zoom-in;
  animation: lbIn .5s var(--ease-out);
}
@keyframes lbIn { from { opacity: 0; transform: scale(.97); } }
.lightbox.is-zoomed .lightbox__img { cursor: grab; }
.lightbox.is-zoomed.is-panning .lightbox__img { cursor: grabbing; transition: none; }

.lightbox__bar {
  position: absolute; left: 50%; bottom: max(20px, 3vh);
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 4px;
  padding: 7px 9px;
  --ui-fg: rgba(255, 255, 255, .94);
  --ui-fg-dim: rgba(255, 255, 255, .56);
  --glass-line: rgba(255, 255, 255, .16);
  color: var(--ui-fg);
}
.lightbox__btn {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border-radius: 11px;
  color: var(--ui-fg-dim);
  transition: color .3s var(--ease-out), background-color .3s var(--ease-out);
}
.lightbox__btn svg { width: 11px; }
.lightbox__btn:hover { color: #fff; background: rgba(255, 255, 255, .12); }
.lightbox__btn[aria-pressed='true'] { color: var(--accent); background: rgba(255, 255, 255, .1); }
.lightbox__meta {
  display: flex; flex-direction: column; gap: 1px;
  padding: 0 12px;
  min-width: 170px; max-width: 44vw;
  text-align: center;
}
.lightbox__meta b { font-size: 12px; font-weight: 590; }
.lightbox__meta i {
  font-style: normal; font-size: 10.5px;
  color: var(--ui-fg-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lightbox__sep { width: 1px; height: 20px; margin: 0 5px; background: var(--glass-line); }

/* --- help ---------------------------------------------------------------- */

.helpbtn {
  position: fixed; z-index: var(--z-top);
  right: var(--edge);
  /* Top edge flush with the top edge of the filmstrip. */
  bottom: calc(var(--dock-pad) + var(--strip-h) - var(--help-size));
  width: var(--help-size); height: var(--help-size);
  display: grid; place-items: center;
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--ui-fg-dim);
  opacity: .55;
}
.helpbtn:hover { opacity: 1; color: var(--ui-fg); }

.helpcard {
  position: fixed; z-index: var(--z-sheet);
  right: var(--edge);
  bottom: calc(var(--dock-pad) + var(--strip-h) + 12px);
  width: min(300px, calc(100vw - 2 * var(--edge)));
  padding: 18px 20px 16px;
  border-radius: 18px;
  animation: sheetIn .4s var(--ease-out);
  transform-origin: bottom right;
}
.helpcard[hidden] { display: none; }
.helpcard h2 {
  margin: 0 0 14px;
  font-family: var(--font-mono);
  font-size: 9px; font-weight: 400; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ui-fg-faint);
}
.helpcard dl { display: flex; flex-direction: column; gap: 9px; margin: 0; }
.helpcard dl > div { display: flex; align-items: center; gap: 12px; }
.helpcard dt { display: flex; gap: 4px; flex: none; width: 84px; }
.helpcard dd { margin: 0; font-size: 12px; color: var(--ui-fg-dim); }
.helpcard kbd {
  display: grid; place-items: center;
  min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: 5px;
  border: 1px solid var(--glass-line);
  background: var(--glass-hover);
  font-family: var(--font-mono); font-size: 9.5px;
}
.helpcard p {
  margin: 15px 0 0; padding-top: 13px;
  border-top: 1px solid var(--glass-line);
  font-size: 11.5px; line-height: 1.5;
  color: var(--ui-fg-faint);
}

/* --- loader -------------------------------------------------------------- */

.loader {
  position: fixed; inset: 0; z-index: var(--z-loader);
  display: grid; place-items: center; align-content: center; gap: 22px;
  background: var(--ink);
  transition: opacity .8s var(--ease-out), visibility .8s;
}
.loader.is-done { opacity: 0; visibility: hidden; }
.loader__mark {
  font-family: var(--font-script);
  font-size: 46px; line-height: 1;
  color: var(--accent);
  text-shadow: 0 0 40px color-mix(in oklab, var(--accent) 55%, transparent);
  animation: pulse 2.4s var(--ease-soft) infinite;
}
@keyframes pulse { 50% { opacity: .45; } }
.loader__rule { width: 104px; height: 1px; background: rgba(255, 255, 255, .14); overflow: hidden; }
.loader__rule i { display: block; width: 0; height: 100%; background: var(--accent); transition: width .5s var(--ease-out); }

.noscript { padding: 12vh 8vw; color: #fff; }
.noscript h1 { font-family: var(--font-script); font-weight: 400; font-size: 44px; margin: 0 0 12px; }

/* --- responsive ---------------------------------------------------------- */

/* Tablet: nav collapses to the sheet, plates grow into the freed space. */
@media (max-width: 1180px) {
  .navbar { display: none; }
  .navmenu { display: flex; }
}

/* The dock stacks here, so its height is the strip plus the read-out row. */
@media (max-width: 860px) {
  :root {
    --dock-h: calc(var(--strip-h) + 60px);
    --plate-h: min(60dvh, calc(90vw / var(--ratio)));
  }
  .brand__text { display: none; }
  .brand { padding: 9px 14px; }
  .dock__inner {
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  }
  .dock__readout { width: auto; max-width: 100%; justify-self: auto; }
  /* Stacked, the read-out no longer stands at the foot of the strip, so there is
     no line left to sign on. */
  .readout__sign { display: none; }
  .filmstrip { max-width: 100%; order: -1; }
  .dock__toggle { display: none; }
  .outro, .divider { width: min(78vw, 340px); }
  .helpbtn { display: none; }
}

/* Phone: a landscape page can only be so wide, so the caption fills the space
   under the plate instead of leaving it empty. */
@media (max-width: 620px) {
  :root {
    --thumb-w: 42px;
    --thumb-h: 30px;
    --thumb-active-w: 58px;
    --thumb-active-h: 41px;
    --thumb-gap: 4px;
    --thumb-group-extra: 7px;
    --strip-pad: 5px;
    --film-pad: 5px;
    --dock-h: calc(var(--strip-h) + 54px);
    --plate-h: min(56dvh, calc(94vw / var(--ratio)));
    --gap: 20px;
  }
  .filmstrip { border-radius: 16px; }
  .strip__arrow { display: none; }
  .dock__readout { padding: 7px 13px; gap: 10px; }
  .readout__caption { display: none; }
  .hint { display: none; }

  .plate__cap {
    top: calc(100% + 20px);
    flex-direction: column; gap: 5px;
    text-align: center; align-items: center;
  }
  .plate__cap span { white-space: normal; max-width: 34ch; }
  .plate__cap::before {
    content: attr(data-chapter);
    font-size: 15px; font-weight: 590;
    color: var(--cap-title);
    order: -1;
  }
}

/* Without hover there is nothing to reveal, so the affordance stays visible. */
@media (hover: none) {
  .plate.is-active .plate__zoom { opacity: .85; transform: none; }
  .plate.is-active .plate__num { opacity: .8; }

  /* A touch screen has no room to hold the last page and the closing note at
     once: the note arrives and the page is left crowding its edge, zoom mark and
     all. So the page gives the note the spread to itself and returns on the way
     back. A computer window has the width for both, and keeps them. */
  .plate { transition: opacity .55s var(--ease-soft); }
  .plate.is-hushed { opacity: 0; pointer-events: none; }
}

/* A short window: the closing column is the tallest thing on the spread, and
   once its spacing has closed up as far as it goes the setting itself draws in,
   so the words stay clear of the bar above and the dock below. */
@media (max-height: 720px) {
  .outro__title { font-size: min(clamp(34px, 4.4vw, 56px), 6.6vh); }
  .outro__lead { font-size: 13px; }
  .outro__link { padding: 10px 16px; }
  .outro__link span { font-size: 13px; }
  .outro__colophon { gap: clamp(9px, 1.6vh, 14px); padding-top: clamp(11px, 2vh, 16px); }
  .outro__credit { gap: 2px; }
  .outro__credit-thanks { margin-bottom: 3px; }
}

/* Short landscape windows: no room for previews, so the page takes the space. */
@media (max-height: 560px) {
  :root {
    --strip-h: 0px;
    --dock-h: 48px;
    --plate-h: min(78dvh, calc(90vw / var(--ratio)));
  }
  .filmstrip { display: none; }
  .plate__cap, .helpbtn, .readout__sign { display: none; }
  /* No band is left between the artwork and the dock to invite the reader
     along, and a phone held this way is swiped without being asked, so the
     invitation goes as it does on a phone held upright. */
  .hint { display: none; }

  /* Two lines of read-out are a tall thing to stand over the artwork in a window
     this shallow, so it wears the pill it wears on a phone held upright: the
     chapter and the count, and the caption left to the page itself. It keeps the
     corner it stands on — the row sets its children on their baseline and the
     dock holds the left edge — so the pill closes down and in from its top right
     rather than moving, and the height it gives back is height the artwork had
     been sharing. */
  .dock__readout {
    width: auto;
    max-width: calc(100vw - 2 * var(--edge));
    padding: 7px 13px;
    gap: 10px;
  }
  .readout__caption { display: none; }
  /* Nothing left for it to show, so the switch that would show it goes as well. */
  .dock__toggle { display: none; }
  /* And the bar is too shallow to carry the name beside the mark, as on a phone
     held upright, so the mark stands for it. */
  .brand__text { display: none; }
  .brand { padding: 9px 14px; }

  /* The chapter sheet draws its rows in for the same reason, and draws them to
     the window it is given, so all ten chapters stand between the bar and the
     foot of a phone held sideways. */
  .sheet__list > li > a {
    padding: clamp(5px, 1.6vh, 12px) 13px;
    font-size: 12.5px; line-height: 1.2;
  }
  .sheet__rule { margin: 4px 10px; }

  /* Nothing this short can hold the closing column as written, so it spends the
     width it does have: the panel widens, the two addresses stand side by side,
     and the lead paragraph goes — the page resting beside it says the same words
     in Wynn's own setting. What is left is the invitation, the addresses and the
     colophon, which is the whole of what the spread is for. */
  .outro { width: min(88vw, 620px); }
  .outro__lead { display: none; }
  .outro__links { flex-direction: row; }
  .outro__link { flex: 1; min-width: 0; }
  .outro__link b { width: auto; }
}

/* --- motion & contrast preferences -------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .plate__hit { opacity: 1; transform: none; filter: none; }
  .divider { opacity: 1; }
  .ambient__grain { display: none; }
}

@media (prefers-contrast: more) {
  :root[data-chrome='dark'] { --ui-fg-dim: rgba(255, 255, 255, .8); --ui-fg-faint: rgba(255, 255, 255, .6); }
  :root[data-chrome='light'] { --ui-fg-dim: rgba(0, 0, 0, .78); --ui-fg-faint: rgba(0, 0, 0, .6); }
  .glass { background-color: color-mix(in srgb, var(--page-avg) 70%, transparent); }
  .plate__hit { opacity: 1; }
}
