/* =============================================================
   Palimpsest — Interactive home ("a scaled paper model") with
   in-scene hotspots, + editorial "draft paper" level detail,
   + the "step into the memory" transition.
   Consumes tokens from ../../colors_and_type.css
   ============================================================= */

:root {
  --memory-paper: #d3cdc5;   /* warm light grey, matches the render surround */
  --memory-paper-2: #d3cdc5; /* lighter wash */
  --doc-paper: #FAF8F4;      /* Parchment 100 — detail page ground */
  /* website override: do NOT use Cascadia Mono anywhere on the site —
     fall back to the specified body family for all --font-mono usages. */
  --font-mono: var(--font-body);
  /* shared height at which page content begins below the fixed top bar.
     The bar's fade veil is now faint, so this only needs to clear the bar
     itself — kept identical on Detail / About / Contact. */
  --content-top: 98px;
  --video-inset: 0px;
}

* { box-sizing: border-box; }
/* belt-and-suspenders so a photo never renders wider than its container,
   regardless of its own intrinsic pixel size — .crop below already does
   width:100%, but a flex/grid item's own min-width:auto can still refuse
   to shrink below an image's intrinsic min-content size even when the
   image itself is capped, which is what max-width (not just width) here
   guards against. */
img { max-width: 100%; height: auto; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
  background: var(--memory-paper);
  color: var(--ink);
  font-family: var(--font-body); font-weight: 300;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
#root { height: 100%; }
button { font-family: inherit; }
::selection { background: var(--sand-300); color: var(--ink); }

/* Film grain — texture + transition wash */
.ps-grain {
  position: absolute; inset: -50%; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.05; mix-blend-mode: multiply;
  animation: ps-grain-shift 1.1s steps(3) infinite;
}
@keyframes ps-grain-shift { 0%{transform:translate(0,0)} 33%{transform:translate(-3%,2%)} 66%{transform:translate(2%,-2%)} 100%{transform:translate(0,0)} }
@media (prefers-reduced-motion: reduce) { .ps-grain { animation: none; } }

.ps-stage { position: fixed; inset: 0; overflow: hidden; }
.ps-screen { position: absolute; inset: 0; }

/* =============================================================
   HOME — full-bleed scaled paper model + hotspots
   ============================================================= */
.ps-home { position: absolute; inset: 0; background: #7c7c7c; overflow: hidden; }

/* Shown (see homeLoading in Experience.jsx) while either the idle video's
   own metadata or the scene data from Sanity is still pending — desktop
   and mobile both, unlike most of the rest of this file: fitting is
   universal now (see fitHotspots), so both can genuinely have a moment
   before the room is actually usable, not just mobile. Indeterminate
   sweep (no real "% loaded" figure exists to show honestly) rather than a
   spinner, to match the plain-rectangle, unornamented language the rest of
   the site's UI uses. */
.ps-loading {
  position: absolute; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.ps-loading-bar {
  position: relative;
  width: 180px; height: 3px;
  background: rgba(255, 255, 255, 0.18);
  overflow: hidden;
}
.ps-loading-bar::after {
  content: "";
  position: absolute; top: 0; left: -40%;
  width: 60%; height: 100%;
  background: #ffffff;
  animation: ps-loading-sweep 1.5s ease-in-out infinite;
}
@keyframes ps-loading-sweep {
  0%   { left: -40%; }
  100% { left: 100%; }
}

.ps-scene-video {
  position: absolute;
  top: var(--video-inset); left: var(--video-inset);
  width: calc(100% - 2 * var(--video-inset)); height: calc(100% - 2 * var(--video-inset));
  object-fit: contain; display: block;
  opacity: 0;
  transition: opacity 0.8s var(--ease-drift);
}
.ps-scene-video.is-recalled { opacity: 1; }
.ps-idle-video { opacity: 1; transition: none; }

.ps-ambient-layer {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
}
/* Was .ps-ambient-layer::after — the LAYER itself stays full-screen
   (inset:0, exactly as before) so its dark edges always reach every side
   of the viewport regardless of the video's own fitted box; only the
   BRIGHT OVAL within it needs to track the video's proportions rather than
   the screen's. That's the one part of the gradient this plain percentage
   version (55%/85% of this element's own full-screen box) can't express —
   fitHotspots in Experience.jsx overrides backgroundImage with an
   equivalent gradient sized in pixels off the video's real fitted
   dimensions instead of this element's own; this plain version is only
   what's visible for the brief moment before that first fit lands (see
   .ps-hotspots--fitting for the same window on the hotspot layer). Stops
   below are a smoothstep (3t²-2t³) ease from the clear center (which ends
   at 22% — keep this in sync with BRIGHT_END in vignetteStops(),
   Experience.jsx) out to the dark edge — same curve, same reasoning as
   .ps-detail-head's own veil (see experience.css): a couple of stops reads
   as a visible ring where the falloff kinks; more stops on a curve reads
   as a smooth vignette. */
.ps-vignette {
  position: absolute; inset: 0; z-index: 1;
  background-image: radial-gradient(ellipse 55% 85% at 50% 50%,
    transparent 0%, transparent 22%,
    rgba(0,0,0,0.014) 29.8%, rgba(0,0,0,0.052) 37.6%, rgba(0,0,0,0.108) 45.4%, rgba(0,0,0,0.176) 53.2%,
    rgba(0,0,0,0.25) 61%, rgba(0,0,0,0.324) 68.8%, rgba(0,0,0,0.392) 76.6%, rgba(0,0,0,0.448) 84.4%,
    rgba(0,0,0,0.486) 92.2%, rgba(0,0,0,0.5) 100%);
  pointer-events: none;
}
.ps-ambient {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; pointer-events: none;
  filter: blur(72px);
  transform: scale(2);
  opacity: 0;
  transition: opacity 0.8s var(--ease-drift);
}
.ps-ambient.is-recalled { opacity: 0.6; }
.ps-idle-ambient { opacity: 0.4; transition: none; }

/* top chrome — tweak-driven via inline --bar-* vars (no token declarations here) */
.ps-home-top {
  position: absolute; top: 0; left: 0; right: 0; z-index: 8;
  display: flex; align-items: center; justify-content: space-between;
  padding: 26px 46px; gap: 24px;
}
/* layout variants */
.ps-home-top[data-layout="centered"] {
  flex-direction: column; align-items: center; gap: 14px; text-align: center;
}
.ps-home-top[data-layout="left"] {
  justify-content: flex-start; align-items: baseline; gap: 40px;
}
.ps-home-mark {
  font-family: var(--bar-font, var(--font-display)); font-weight: 400; font-size: var(--bar-mark-size, 22px); letter-spacing: .02em;
  color: var(--bar-color, #4A463F); background: none; border: 0; padding: 0;
  mix-blend-mode: normal;
}
.ps-home-nav { display: flex; gap: 30px; }
.ps-home-top[data-layout="centered"] .ps-home-nav { gap: 26px; }
.ps-home-nav a {
  font-family: var(--bar-font, var(--font-display)); font-weight: 400; font-size: var(--bar-link-size, 16px); color: var(--bar-color, #4A463F); cursor: pointer;
  text-decoration: none; mix-blend-mode: normal;
  transition: color var(--dur-base) var(--ease-drift);
}
.ps-home-nav a:hover { color: #ffffff; }

/* "About" nav item's hover dropdown (see AboutNavItem in Primitives.jsx) —
   shared by ps-home-nav and ps-detail-head .nav, so it's written once here
   rather than per-header. Portaled onto document.body (every header here
   uses mask-image for its fade veil, which clips its whole subtree to its
   own short box — a tall dropdown nested inside would be masked away past
   the header's own height), so .nav-drop is positioned via inline left/top
   computed from the trigger's rect (left-aligned to it, not centered), not
   CSS containment, and its links need their own font/color/hover — they're
   no longer descendants of ps-home-nav/.nav so those "nav a" rules don't
   reach them. No background/veil treatment on the panel for now — plain
   text list. */
.nav-item-drop { position: relative; display: inline-flex; align-items: center; }
.nav-drop { position: fixed; z-index: 9999; animation: nav-drop-in var(--dur-base) var(--ease-drift) both; }
@keyframes nav-drop-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Full-screen scrim behind the centered mobile menu (see nav-drop--centered
   below) — dims the page so the menu text stays legible. z-index sits just
   under .nav-drop's 9999 so the panel stays on top of it. Plain (black) is
   Home's — it sits over the busy scene render, where black reads clearly;
   --paper (parchment-toned, matching --memory-paper) is Detail's, and
   mirrors pages.css's identical scrim for About/Contact/Devlog, which sit
   on a flat parchment ground where black would clash instead of blending. */
.nav-drop-scrim {
  position: fixed; inset: 0; z-index: 9998;
  background: rgba(0, 0, 0, 0.55);
  animation: nav-drop-scrim-in var(--dur-base) var(--ease-drift) both;
}
.nav-drop-scrim--paper { background: rgba(211, 205, 197, 0.85); }
@keyframes nav-drop-scrim-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* blurred backdrop for the whole panel (shadow==="paper" only — see
   AboutNavItem in Primitives.jsx). Has to live here, on .nav-drop itself
   (the outermost portaled fixed element), not on the individual links —
   backdrop-filter on a descendant several levels deep inside a portaled
   position:fixed tree empirically doesn't composite/render at all, the
   same way .pg-head's own working blur is on its own outermost fixed
   element, never a descendant of it. mask-image feathers the rectangular
   edge itself (rather than an oval unrelated to the panel's actual shape)
   so the blur fades out right at the buttons' own edges — two linear
   fades, one per axis, intersected into a soft-edged rectangle. Each fade
   uses the same many-stops ease-out-decay curve as .ps-detail-head's own
   veil above (a couple of stops reads as a visible kink; more stops on a
   curve reads as smooth), mirrored at both ends since this panel fades on
   every side rather than just one. The opaque plateau runs 15%-85% (rather
   than a wider ramp eating further in) so most of the panel stays fully
   blurred/opaque — only the outer 15% on each side tapers off — instead of
   the content behind reading through around the text. */
.nav-drop--blur {
  background: rgba(211, 205, 197, 0.15);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border-radius: 12px;
  padding: 50px 45px;
  /* this padding pushes the box's own top edge above the "About" trigger
     (so the visible text can sit a fixed distance below it — see
     TEXT_TOP_GAP in AboutNavItem) — that overlapping strip is masked
     invisible but, without this, would still intercept clicks meant for
     "About" itself. pointer-events:none here (with :auto restored on
     .nav-drop-panel below, where the actual links live) lets clicks fall
     through the padding to whatever's under it. */
  pointer-events: none;
  mask-image:
    linear-gradient(to bottom,
      transparent 0%, rgba(0,0,0,0.22) 3%, rgba(0,0,0,0.50) 6%, rgba(0,0,0,0.75) 9%, rgba(0,0,0,0.92) 12%, black 15%,
      black 85%, rgba(0,0,0,0.92) 88%, rgba(0,0,0,0.75) 91%, rgba(0,0,0,0.50) 94%, rgba(0,0,0,0.22) 97%, transparent 100%),
    linear-gradient(to right,
      transparent 0%, rgba(0,0,0,0.22) 3%, rgba(0,0,0,0.50) 6%, rgba(0,0,0,0.75) 9%, rgba(0,0,0,0.92) 12%, black 15%,
      black 85%, rgba(0,0,0,0.92) 88%, rgba(0,0,0,0.75) 91%, rgba(0,0,0,0.50) 94%, rgba(0,0,0,0.22) 97%, transparent 100%);
  mask-composite: intersect;
  -webkit-mask-image:
    linear-gradient(to bottom,
      transparent 0%, rgba(0,0,0,0.22) 3%, rgba(0,0,0,0.50) 6%, rgba(0,0,0,0.75) 9%, rgba(0,0,0,0.92) 12%, black 15%,
      black 85%, rgba(0,0,0,0.92) 88%, rgba(0,0,0,0.75) 91%, rgba(0,0,0,0.50) 94%, rgba(0,0,0,0.22) 97%, transparent 100%),
    linear-gradient(to right,
      transparent 0%, rgba(0,0,0,0.22) 3%, rgba(0,0,0,0.50) 6%, rgba(0,0,0,0.75) 9%, rgba(0,0,0,0.92) 12%, black 15%,
      black 85%, rgba(0,0,0,0.92) 88%, rgba(0,0,0,0.75) 91%, rgba(0,0,0,0.50) 94%, rgba(0,0,0,0.22) 97%, transparent 100%);
  -webkit-mask-composite: source-in;
}
/* Alternative to .nav-drop--blur above, used on Detail's desktop dropdown
   only (see blurOnDesktop on AboutNavItem) — a plain translucent rectangle
   rather than a blurred one. No backdrop-filter/mask-image at all: a flat,
   sufficiently opaque fill mostly replaces whatever's behind it outright,
   so the dark ink text reads clearly regardless of the video's own
   brightness — unlike blur, which still shows through however dark or
   busy the underlying frame happens to be. Doesn't need to live on the
   outermost portaled element the way backdrop-filter does (no compositing
   quirk with a plain background-color), but stays there anyway for the
   same tight-hugging padding this replaced. */
.nav-drop--scrim {
  background: rgba(211, 205, 197, 0.85);
  padding: 5px 5px;
}
.nav-drop-panel { display: flex; flex-direction: column; gap: 4px; min-width: 240px; pointer-events: auto; }
.nav-drop-panel a {
  display: block; padding: 8px 8px; white-space: nowrap;
  font-family: var(--font-display); font-weight: 400; font-size: 16px;
  color: inherit; text-decoration: none;
  transition: color var(--dur-base) var(--ease-drift);
}
.nav-drop-panel a:hover { color: #ffffff; }
/* soft faded halo lifting each option off whatever's behind it, rather
   than a hard box-shadow (see the shadow prop on AboutNavItem) — black on
   Home, since that dropdown sits over the busy scene render; memory-paper
   everywhere else (mirrored in pages.css), for a flat page background.
   Both variants get the halo; only "paper" (Detail) also gets
   nav-drop--blur above. */
/* the dark halo alone reads fine against Home's typically bright/busy
   scene renders (its whole reason for existing), but the dropdown's text
   color is itself a dark ink tone (snapshotted from the trigger's own
   resting color — see restColorRef in Primitives.jsx), so on a darker
   stretch of video there's nothing light anywhere to actually create
   contrast against — dark text, dark shadow, dark video. Two extra tight,
   small-blur white layers add a thin light rim right at each glyph's edge
   without touching color, scrim, or anything else: barely visible over a
   bright background (where the dark halo still dominates), but enough to
   keep the text legible over a dark one. */
.nav-drop-panel--shadow-black a {
  text-shadow: 0 0 3px rgba(0,0,0,0.55), 0 0 7px rgba(0,0,0,0.32), 0 0 15px rgba(0,0,0,0.12);
}
.nav-drop-panel--shadow-paper a {
  text-shadow: 0 0 3px var(--memory-paper), 0 0 7px var(--memory-paper), 0 0 15px var(--memory-paper);
}

/* hotspots — invisible interactive regions laid over objects in the render */
.ps-hotspots {
  position: absolute;
  inset: 0;
  z-index: 6;
  opacity: 1;
  transition: opacity 0.25s var(--ease-drift);
}
/* hidden and inert until fitBounds is ready (see the JSX), so nothing is
   ever clickable/visible in the wrong place while it's still fitting to
   the video's real dimensions. */
.ps-hotspots--fitting { opacity: 0; pointer-events: none; }
.ps-hotspot {
  position: absolute; transform: translate(-50%, -50%);
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
}
.ps-hotspot:focus-visible {
  outline: 2px solid var(--ink-500); outline-offset: 6px; border-radius: 10px;
}
/* Mobile-only "this is tappable" hint — a small ripple expanding outward
   from the hotspot's own center point, hidden by default (desktop relies
   on hover/cursor affordance instead, which touch doesn't have). left:50%;
   top:50% places its origin at the hotspot's exact center regardless of
   the hotspot's own box being non-square (hotW/hotH differ per hotspot);
   translate(-50%,-50%) centers the ring itself on that point. That
   translate has to be baked into every step of the keyframes below rather
   than left as this rule's own static transform — a CSS animation's
   transform keyframes fully REPLACE any static transform declaration for
   as long as the animation is active/filled (same issue as
   nav-scatter-in/nav-drop-in-centered elsewhere in this file), so a plain
   "transform: scale(...)" keyframe would silently un-center the ring the
   whole time, not just fail to animate it. Staggered per hotspot via
   inline animation-delay (see the JSX) so they pulse in sequence, like the
   room is gently drawing your eye around it, rather than all five
   flashing at once. */
.ps-hotspot-hint { display: none; }
@media (hover: none) and (pointer: coarse) {
  .ps-hotspot-hint {
    display: block;
    position: absolute; left: 50%; top: 50%;
    width: 12px; height: 12px;
    border-radius: 50%;
    border: 2px solid #ffffff;
    opacity: 0;
    pointer-events: none;
    animation: ps-hotspot-hint 5s ease-out infinite;
  }
  /* stops every ripple once a hotspot has been clicked/committed — see the
     active != null check in Experience.jsx. Same pattern as the
     body.mobile-menu-open suppression below. */
  .ps-hotspot-hint.is-committed { display: none; }
}
@keyframes ps-hotspot-hint {
  0%, 60% { opacity: 0; transform: translate(-50%, -50%) scale(1); }
  68%     { opacity: 0.9; transform: translate(-50%, -50%) scale(1); }
  85%     { opacity: 0; transform: translate(-50%, -50%) scale(3); }
  100%    { opacity: 0; transform: translate(-50%, -50%) scale(1); }
}

.ps-home-hint {
  position: absolute; left: 0; right: 0; bottom: 4%; z-index: 7; text-align: center;
  font-size: 11px; letter-spacing: var(--tracking-eyebrow); text-transform: uppercase;
  color: var(--stone-600); text-shadow: 0 1px 14px rgba(214,210,203,0.9);
  display: flex; align-items: center; justify-content: center; gap: 9px; pointer-events: none;
}
.ps-home-hint .ph-light { color: #ffffff; }

/* =============================================================
   HOME — mobile/touch. Gated on (hover:none) and (pointer:coarse), never
   on viewport width alone, so resizing a desktop browser window narrow
   never triggers any of this — only a real touch device (or DevTools'
   device toolbar, which sets the same media features) does. See
   isTouchDevice in Primitives.jsx for the JS-side equivalent of this
   check (disabling the cursor trail, switching hotspots to tap-only).
   ============================================================= */
.mobile-menu-trigger { display: none; }
@media (hover: none) and (pointer: coarse) {
  .ps-home-top { padding: 14px 18px; }
  /* overrides the --bar-mark-size/--bar-link-size custom properties'
     resolved font-size (tweak-driven, set inline on .ps-home-top) — a
     plain font-size here wins on the cascade since it comes later in the
     stylesheet, no !important needed. */
  .ps-home-mark { font-size: 17px; }
  /* replaced by .mobile-menu-trigger below (see MobileMenu in
     Primitives.jsx) — the plain 3-link row doesn't fit/read well this
     small, and About's own dropdown needs hover, which touch doesn't have. */
  .ps-home-nav { display: none; }
  .mobile-menu-trigger { display: inline-flex; }
  .mobile-menu-btn {
    background: none; border: 0; padding: 6px; margin: -6px;
    color: var(--bar-color, #4A463F);
    cursor: pointer; -webkit-tap-highlight-color: transparent;
  }
  /* same tone as every other active/hover state on the site (e.g.
     .ps-home-nav a:hover, .nav-drop-panel a:hover) — the cross reads as
     "this button is currently active" rather than just a different icon. */
  .mobile-menu-btn--open { color: #ffffff; }
  /* "About +" / "About −" toggle for the six scene links, expanding in
     place rather than opening a second nested menu. */
  .mobile-menu-about { cursor: pointer; }
  .mobile-menu-sub { padding-left: 20px; font-size: 14px; opacity: 0.85; }
  /* see the body.classList.toggle in MobileMenu, Primitives.jsx — the
     hint would otherwise keep pulsing underneath the menu's scrim. */
  body.mobile-menu-open .ps-hotspot-hint { display: none; }
}
/* Portrait: rather than locking to landscape, the sharp video (idle +
   whichever scene is developed) becomes a square window — 100vw square,
   vertically centered — with object-fit:cover instead of contain. Since
   the video's native aspect ratio is wider than 1:1, cover scales it up
   until its HEIGHT fills that square, leaving its WIDTH wider than the
   square/screen; .ps-home's own overflow:hidden (see HOME section above)
   clips the excess on the left/right rather than needing a wrapper here.
   .ps-hotspots' own fit (computed in JS — see fitHotspots in Experience.jsx)
   mirrors this same cover math so hotspot buttons still land on the
   correct points of the now-cropped, up-scaled video. The blurred ambient
   layer is left alone — it's just decorative wash, not something that
   needs pixel-accurate alignment with hotspots. */
@media (hover: none) and (pointer: coarse) and (orientation: portrait) {
  .ps-scene-video {
    top: 50%; left: 0; transform: translateY(-50%);
    width: 100vw; height: 100vw;
    object-fit: cover;
  }
}
/* Periodic "rotate your device" hint, centered in the blank strip below
   the square video window (top is set inline — see rotateHintTop in
   Experience.jsx). No text, unlike the old full-screen rotate-lock prompt
   this replaced — just the icon, pulsing on the same cadence/shape as
   .ps-hotspot-hint (5s cycle, mostly hidden), with a rotate motion baked
   into the keyframes themselves rather than left as this rule's own static
   transform — a CSS animation's transform keyframes fully REPLACE any
   static transform declaration for as long as the animation is
   active/filled (same issue as .ps-hotspot-hint/nav-scatter-in/
   nav-drop-in-centered elsewhere in this file), so translate(-50%,-50%)
   (needed to center the icon on its own point, same reasoning as
   .ps-hotspot-hint) has to be part of every step below, not set once
   outside the animation. */
.ps-rotate-hint { display: none; }
@media (hover: none) and (pointer: coarse) and (orientation: portrait) {
  .ps-rotate-hint {
    display: block;
    position: fixed; left: 50%;
    color: #ffffff;
    opacity: 0;
    pointer-events: none;
    z-index: 5;
    animation: ps-rotate-hint 5s ease-out infinite;
  }
}
@keyframes ps-rotate-hint {
  0%, 60% { opacity: 0;   transform: translate(-50%, -50%) rotate(0deg); }
  68%     { opacity: 0.9; transform: translate(-50%, -50%) rotate(0deg); }
  76%     { opacity: 0.9; transform: translate(-50%, -50%) rotate(-90deg); }
  84%     { opacity: 0.9; transform: translate(-50%, -50%) rotate(0deg); }
  92%     { opacity: 0;   transform: translate(-50%, -50%) rotate(0deg); }
  100%    { opacity: 0;   transform: translate(-50%, -50%) rotate(0deg); }
}
/* "About" dropdown, centered mode — mobile-only alternative to the plain
   trigger-anchored .nav-drop (see centeredOnMobile on AboutNavItem in
   Primitives.jsx): opens centered on the whole screen instead of anchored
   under "About". Own nav-drop-in-centered keyframes rather than reusing
   nav-drop-in — a CSS animation's transform keyframes fully REPLACE any
   static transform declaration for as long as the animation is
   active/filled, so nav-drop-in's plain translateY(-8px)->translateY(0)
   would silently wipe out the translate(-50%,-50%) centering the whole
   time; baking that into both of this animation's own keyframes keeps the
   centering while still getting the same little slide-in nudge. */
.nav-drop--centered { animation-name: nav-drop-in-centered; }
.nav-drop--centered .nav-drop-panel { align-items: center; }
.nav-drop--centered .nav-drop-panel a { text-align: center; }
@keyframes nav-drop-in-centered {
  from { opacity: 0; transform: translate(-50%, calc(-50% - 8px)); }
  to   { opacity: 1; transform: translate(-50%, -50%); }
}

/* =============================================================
   TRANSITION — "step into the memory"
   data-phase on .ps-stage: idle | entering | open
   ============================================================= */
.ps-home { transform-origin: var(--enter-x, 50%) var(--enter-y, 50%); }
.ps-stage[data-phase="entering"] .ps-home {
  transform: scale(1.18);
  transition: transform 1.2s var(--ease-drift);
}
.ps-stage[data-phase="entering"] .ps-home-top,
.ps-stage[data-phase="entering"] .ps-hotspots,
.ps-stage[data-phase="entering"] .ps-home-hint {
  opacity: 0; transition: opacity .5s var(--ease-drift); pointer-events: none;
}

.ps-wash {
  position: fixed; inset: 0; z-index: 40; pointer-events: none;
  background: var(--memory-paper-2);
  opacity: 0; /* safe resting state if a throttled tab freezes the animation */
}
/* swell while entering, clear once open — keyframes from a safe start so a
   frozen capture degrades to "no wash" rather than a stuck cover */
.ps-stage[data-phase="entering"] .ps-wash { animation: ps-wash-in .82s var(--ease-drift) forwards; }
.ps-stage[data-phase="open"] .ps-wash { animation: ps-wash-out 1s var(--ease-drift) forwards; }
@keyframes ps-wash-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ps-wash-out { from { opacity: 1; } to { opacity: 0; } }
.ps-wash .ps-grain { opacity: 0.16; }

/* detail nudges in (decorative — content shows even if animation is frozen) */
.ps-detail-screen { animation: ps-detail-in .9s var(--ease-drift) both; }
@keyframes ps-detail-in { from { opacity: 0.5; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .ps-detail-screen { animation: none; } }

/* =============================================================
   DETAIL — two independently-scrolling "draft leaves" beneath a
   header that fades out, so faint content shows through underneath.
   ============================================================= */
.ps-detail { position: absolute; inset: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--memory-paper); color: var(--ink); }

/* the page scrolls normally through the hero + brief; ps-split-wrap then
   pins itself to the top of this scroller and the split takes over —
   see ps-split-wrap below. */
.ps-detail-scroll {
  position: relative; flex: 1; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: none; -ms-overflow-style: none;
}
.ps-detail-scroll::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ---- hero video → brief crossfade — a tall (340vh) wrapper whose inner
   stage pins to the top of the viewport for the full scroll distance, so
   scrolling through it doesn't move anything on screen; JS instead reads
   how far the wrapper has scrolled past its pinned position (0→1) and maps
   that straight to opacity — video fades out over the first quarter, brief
   fades in slowly over the next half, then holds fully visible (still
   pinned) for the last quarter before it un-pins back to normal document
   scroll at progress 1 (see the phase comment above heroVideoOpacity in
   Experience.jsx). Being a pure function of scroll position (no triggered
   animation/timers) is what makes it reversible: scrolling back up
   retraces the exact same opacities. */
.ps-hero-pin-wrap { position: relative; height: 340vh; }
.ps-hero-pin-stage { position: sticky; top: 0; height: 100vh; overflow: hidden; background: var(--memory-paper); }
.ps-hero-pin-stage .ps-hero-video, .ps-hero-pin-stage .ps-brief { position: absolute; inset: 0; }
/* Touch: plain default scrolling instead of the pin/crossfade above — see
   heroVideoOpacity/heroBriefOpacity in Experience.jsx, which stay at a
   constant 1 (both fully visible) on touch rather than computing the
   scroll-driven curve. Turns the pin stage back into two normal, stacked
   blocks (video, then brief, in document flow) instead of both absolutely
   positioned on top of each other waiting to be crossfaded. */
@media (hover: none) and (pointer: coarse) {
  .ps-hero-pin-wrap { height: auto; }
  .ps-hero-pin-stage { position: static; height: auto; overflow: visible; display: block; }
  /* .ps-hero-video keeps position:relative (not static like .ps-brief) —
     .ps-hero-sound is position:absolute and anchors to whichever ancestor
     is positioned first; without this it walks all the way up past the
     now-static .ps-hero-pin-stage and lands outside the video entirely. */
  .ps-hero-pin-stage .ps-hero-video { position: relative; inset: auto; }
  .ps-hero-pin-stage .ps-brief { position: static; inset: auto; }
}
.ps-hero-video {
  width: 100%; height: 100vh; margin: 0 auto;
  overflow: hidden; background: #000;
}
/* height override has to live down here, after the unconditional rule
   right above it — same selector, same specificity, so whichever comes
   later in the file wins; a copy up in the touch block near the top of
   this file (where it used to be) just got silently beaten by the 100vh
   above regardless of the media query matching. 100dvh (not 60vh, which
   was tried first) — dvh tracks the browser's actual visible viewport,
   shrinking/growing live as the address bar/toolbar chrome shows or
   hides, unlike 100vh which is always the LARGEST possible viewport and
   so left .ps-hero-sound's bottom:32px anchored past what was really
   visible; 60vh sidestepped that by just making the box shorter, but that
   left the brief peeking in at the bottom of the screen underneath the
   hero, which isn't wanted — dvh gets a genuinely full-screen video AND
   keeps the corner buttons on-screen. Browsers without dvh support simply
   fail to parse this declaration and keep the 100vh above. */
@media (hover: none) and (pointer: coarse) {
  .ps-hero-video { height: 100dvh; }
}
.ps-hero-video video { display: block; width: 100%; height: 100%; object-fit: cover; }
/* covers the native poster→first-frame swap (see the onPlaying comment in
   Experience.jsx) — sits above the video by DOM order alone at first, but
   z-index is explicit since it also needs to out-rank the sound button. */
.ps-hero-video-poster {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; object-fit: cover;
  opacity: 1; transition: opacity 200ms linear;
  pointer-events: none;
}
.ps-hero-sound {
  position: absolute; right: 32px; bottom: 32px; width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(46,44,40,0.6); border: 1px solid rgba(250,248,244,0.4);
  border-radius: 50%; color: var(--paper); cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-drift), color var(--dur-base) var(--ease-drift);
}
/* same size/style as .ps-hero-sound above, just centered horizontally
   instead of pinned to the right corner, and at the same bottom offset. */
.ps-hero-scroll-down {
  position: absolute; left: 50%; bottom: 32px; transform: translateX(-50%);
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(46,44,40,0.6); border: 1px solid rgba(250,248,244,0.4);
  border-radius: 50%; color: var(--paper); cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-drift), color var(--dur-base) var(--ease-drift);
}
.ps-hero-scroll-down:hover { border-color: #ffffff; color: #ffffff; }
/* not needed on touch — normal swipe/scroll already gets past the hero
   without it, and it was one more thing competing for space with the
   sound toggle in an already-tight corner. */
@media (hover: none) and (pointer: coarse) {
  .ps-hero-scroll-down { display: none; }
}

/* ---- brief — short intro. Nested in the pin stage it's centered on the
   screen and crossfades in (see above); standalone (no hero video on this
   scene) it falls back to sitting in normal document flow. */
/* container-type:inline-size (not just relying on the touch text-align:left
   override further down) — lets the general "no justify under 320px"
   rule below key off this box's own rendered width rather than the
   viewport, since a narrow *box* (this one shrunk by its own layout, not
   necessarily a phone) is the actual thing that makes justify look bad —
   uneven, overstretched word gaps.
   width:100% (not just max-width) turned out to matter here — this box
   is a flex CONTAINER with position:absolute;inset:0 once pinned (see
   .ps-hero-pin-stage .ps-brief below), and "auto" width in that context
   normally resolves via shrink-to-fit, which looks at content size to
   decide how wide to stretch. Size containment explicitly forbids a
   container from being sized by its own content — that's the entire
   point of it — so with only max-width and no explicit width, the used
   width collapsed to ~0 (just this element's own padding) the moment
   container-type was added. Explicit width:100% gives it a value to
   size from that doesn't depend on content, so containment can't zero it
   out; max-width:720px still caps it exactly as before. */
.ps-brief { width: 100%; max-width: 720px; margin: 0 auto; padding: 384px 46px; text-align: justify; container-type: inline-size; }
.ps-brief p, .ps-brief ol, .ps-brief ul { font-size: var(--body-lg); line-height: var(--leading-relaxed); color: var(--ink); margin: 0; font-style: italic; }
.ps-brief ol, .ps-brief ul { padding-left: 1.4em; }
.ps-brief p + p, .ps-brief p + ol, .ps-brief p + ul,
.ps-brief ol + p, .ps-brief ul + p, .ps-brief ol + ol, .ps-brief ul + ul { margin-top: 0.9em; }
/* general rule: any justified text box that's actually narrower than
   320px switches to left-aligned — justify on a very narrow column
   produces a handful of words stretched with ugly, uneven gaps to fill
   the line, worse than the ragged-right edge it's avoiding. Container
   query (not a viewport media query) since what matters is this box's
   own rendered width, not the screen's — .ps-entry-text's right column
   (minmax(0,400px)) or .ps-entry-fig's caption can narrow past 320px on a
   mid-width viewport even where the *page* itself isn't especially
   narrow. Mirrors identical rules on every other justified text block in
   this file and pages.css. */
@container (max-width: 320px) {
  .ps-brief p, .ps-brief ol, .ps-brief ul { text-align: left; }
}
.ps-hero-pin-stage .ps-brief { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 46px; }
.ps-hero-pin-stage .ps-brief p, .ps-hero-pin-stage .ps-brief ol, .ps-hero-pin-stage .ps-brief ul { max-width: 720px; }
/* padding override for touch has to live down here, after the unconditional
   rule right above it — same selector, same specificity, so whichever
   comes later in the file wins; a copy earlier in the stylesheet (up by
   .ps-hero-pin-wrap) would just get silently beaten by the 0 46px above. */
@media (hover: none) and (pointer: coarse) {
  /* top padding is 96px (not a plain symmetric 32px) so the gap above the
     brief text visually matches the gap below it: below, the 32px here is
     joined by .ps-split-wrap's 56px margin-top and .ps-leaf .ps-col-inner's
     8px padding-top (32 + 56 + 8 = 96) before the next section's content
     actually starts, but nothing else adds space above — the video sits
     flush against the brief with no margin of its own. */
  .ps-hero-pin-stage .ps-brief { padding: 96px 24px 32px; }
  /* justify reads badly on a narrow phone column — ragged-but-readable
     left alignment instead of the uneven word-spacing justify produces
     when there's little width to distribute it across. Same selector,
     same specificity as the unconditional rule above, so this has to stay
     after it in the file to actually win (see the padding override's own
     comment just above for the identical reasoning). */
  .ps-brief { text-align: left; }
}

/* the two halves — split exactly down the middle; EACH column scrolls
   independently (its own overflow container). ps-split-wrap pins to the
   top of ps-detail-scroll once its natural scroll position reaches it —
   from then on, wheel input goes to whichever pane is under the cursor
   (each pane is its own scroll container) rather than the outer page.
   The panes stay non-scrolling (overflow hidden) until JS confirms the
   wrapper is actually stuck and filling the viewport (.is-pinned) — so
   internal scrolling never kicks in mid-transition, only once the split
   covers the whole screen. */
/* scroll-margin-top:120px — matches .ps-detail-head's own total height
   exactly (see the comment there), so the new scroll-down button's
   scrollIntoView({block:"start"}) (Experience.jsx) lands this element's
   top right where the header veil ends rather than at the literal
   viewport top, which would otherwise scroll it in underneath the veil.
   Applies the same in both the sticky/pinned (desktop) and static/normal-
   flow (mobile, see the max-width:1000px override below) cases — how the
   element is positioned doesn't change where scrollIntoView is allowed to
   stop short of its actual top edge. Keep this equal to .ps-detail-head's
   own total padding — the two are a pair, not independent numbers. */
.ps-split-wrap { position: sticky; top: 0; height: 100vh; margin-top: 56px; }
.ps-split { position: relative; z-index: 1; height: 100%; display: grid; grid-template-columns: 1fr 1fr; }
.ps-leaf, .ps-plates { position: relative; display: block; height: 100%; min-height: 0; overflow-y: hidden; overflow-x: hidden; scrollbar-width: none; -ms-overflow-style: none; }
.ps-split-wrap.is-pinned .ps-leaf, .ps-split-wrap.is-pinned .ps-plates { overflow-y: auto; }
.ps-leaf::-webkit-scrollbar, .ps-plates::-webkit-scrollbar { width: 0; height: 0; display: none; }
.ps-plates { border-left: 1px solid var(--stone-400); }

/* header veil — spans both panes, fixed at top (must stay pinned while the
   columns scroll beneath it, unlike ps-home-top which never scrolls — that's
   the one required structural difference now that pg-head also has the
   middle "here" label). Every other property (padding, gap, font, size,
   color, hover) is copied exactly from ps-home-top/ps-home-mark/ps-home-nav
   so the bar reads as visually identical; pg-head mirrors this one 1:1. */
/* Split into two stacked pieces so the layering can be: dropdown's own
   text > dropdown's blur > this veil > page content. .ps-detail-head is
   now the veil ONLY (no children — see PageHead/Experience.jsx), kept at
   its original low z-index so it stays under .nav-drop's blur. The actual
   text (mark/here/nav) lives in the sibling .ps-detail-head-content below,
   which has no background of its own and sits at a z-index above
   .nav-drop's 9999 — position:fixed always opens its own stacking context,
   so a child can never out-rank a z-index:9999 sibling like .nav-drop no
   matter its own z-index; splitting the veil and the text into separate
   fixed elements is the only way to put one above and the other below it. */
.ps-detail-head {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  padding: 120px 0px 0px; pointer-events: none;
  /* plateau (flat, full-opacity) now ends at 21.5% — pinned to the same
     absolute pixel position it was at before this edit (32.2% of the old
     80px total ≈ 25.8px; 25.8/120 ≈ 21.5%), so the color-ramping region
     itself (21.5%-100%) is exactly 40px wider (was ~54px over the old
     80px total, now ~94px over 120px) rather than the plateau shrinking
     or the ramp's own shape changing — same 8-stop smoothstep (3t²-2t³)
     curve, just re-mapped onto the wider span. */
  background: linear-gradient(180deg,
     rgba(211,205,197,0.60) 0%, rgba(211,205,197,0.60) 21.5%,
     rgba(211,205,197,0.567) 32.7%, rgba(211,205,197,0.481) 43.9%,
     rgba(211,205,197,0.364) 55.1%, rgba(211,205,197,0.236) 66.3%,
     rgba(211,205,197,0.119) 77.6%, rgba(211,205,197,0.033) 88.8%,
     rgba(211,205,197,0) 100%);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  /* fade the blur out over the same curve/distance as the color veil, so
     content eases from sharp to blurred instead of cutting off hard */
  mask-image: linear-gradient(180deg,
     rgba(0,0,0,1) 0%, rgba(0,0,0,1) 21.5%,
     rgba(0,0,0,0.945) 32.7%, rgba(0,0,0,0.801) 43.9%,
     rgba(0,0,0,0.606) 55.1%, rgba(0,0,0,0.394) 66.3%,
     rgba(0,0,0,0.198) 77.6%, rgba(0,0,0,0.056) 88.8%,
     rgba(0,0,0,0) 100%);
  -webkit-mask-image: linear-gradient(180deg,
     rgba(0,0,0,1) 0%, rgba(0,0,0,1) 21.5%,
     rgba(0,0,0,0.945) 32.7%, rgba(0,0,0,0.801) 43.9%,
     rgba(0,0,0,0.606) 55.1%, rgba(0,0,0,0.394) 66.3%,
     rgba(0,0,0,0.198) 77.6%, rgba(0,0,0,0.056) 88.8%,
     rgba(0,0,0,0) 100%);
}
.ps-detail-head-content {
  position: fixed; top: 0; left: 0; right: 0; z-index: 10000;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 26px 46px 70px; gap: 24px; pointer-events: none;
}
.ps-detail-head-content > * { pointer-events: auto; }
.ps-detail-head-content .mark, .ps-detail-head-content .here, .ps-detail-head-content .nav a {
  mix-blend-mode: normal;
}
.ps-detail-head-content .mark { font-family: var(--font-display); font-weight: 400; font-size: 21px; color: var(--ink-500); justify-self: start; background: none; border: 0; padding: 0; cursor: pointer; letter-spacing: .02em; transition: color var(--dur-base) var(--ease-drift); }
.ps-detail-head-content .mark:hover { color: #ffffff; }
.ps-detail-head-content .here { font-family: var(--font-display); font-weight: 400; font-size: 22px; color: var(--ink-500); }
.ps-detail-head-content .nav { justify-self: end; display: flex; gap: 30px; }
.ps-detail-head-content .nav a { font-family: var(--font-display); font-weight: 400; font-size: 16px; color: var(--ink-500); cursor: pointer; text-decoration: none; transition: color var(--dur-base) var(--ease-drift); }
.ps-detail-head-content .nav a:hover { color: #ffffff; }
/* mirrors the Home mobile header treatment (see .ps-home-top etc. above) —
   including swapping .nav for .mobile-menu-trigger (see MobileMenu in
   Primitives.jsx); .mobile-menu-btn/--open/-about/-sub are the same global
   rules Home's own block above already defines, not redeclared here.
   justify-self:end matches what .nav itself had, since .mobile-menu-trigger
   takes over its grid cell (3rd column) once .nav is display:none. */
@media (hover: none) and (pointer: coarse) {
  .ps-detail-head-content { padding: 14px 18px; }
  .ps-detail-head-content .mark, .ps-detail-head-content .here { font-size: 17px; }
  .ps-detail-head-content .nav { display: none; }
  .ps-detail-head-content .mobile-menu-trigger { justify-self: end; }
  /* auto (not the base rule's 1fr) for the outer columns — they size to
     their own content ("Palimpsest" / the hamburger icon) instead of
     each claiming an equal fr share, freeing the rest for the title.
     minmax(0,1fr) (not the base rule's auto) for the middle column lets
     it actually shrink below the title's own natural width when there
     isn't room, rather than forcing the grid to overflow — which is what
     was pushing a long scene title (e.g. "The Man With Three Names") onto
     a second line. See the hereRef effect in Experience.jsx for the JS
     that scales .here's font-size down to fit this now-shrinkable box
     exactly, rather than just clipping it. */
  .ps-detail-head-content { grid-template-columns: auto minmax(0, 1fr) auto; }
  .ps-detail-head-content .here {
    display: block; max-width: 100%; overflow: hidden;
    white-space: nowrap; text-align: center;
  }
}

/* both columns share one inner wrapper — tiny side margins (the reference is
   nearly flush to the edges and the center divider). Content starts at
   --content-top, clearing the fixed top bar. */
.ps-col-inner { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 100%; padding: var(--content-top) 8px 8px; }

/* entry — a large full-width plate, its caption sitting full-width directly
   below it (reference), then a text row: subtitle LEFT, description RIGHT.
   No horizontal rules. */
/* min-width:0 — .ps-entry is a flex item in .ps-col-inner's flex column;
   without it, a flex item's default min-width:auto lets its min-content
   size (which can include an oversized figure's own contribution) refuse
   to shrink below that, pushing the whole column wider than the screen. */
.ps-entry { padding-top: 0; min-width: 0; }
/* 64px — matches About's own hero-to-hero gap on desktop (see .pg-hero +
   .pg-hero, pages.css) rather than the tighter 30px used for About's
   OTHER section-to-section rhythm; the touch override below drops it back
   to that same 30px, again mirroring About's own touch-only reduction of
   its hero gap. */
.ps-entry + .ps-entry { padding-top: 64px; }

.ps-entry-fig { margin: 0; display: grid; grid-template-columns: 1fr minmax(0, 400px); column-gap: 24px; row-gap: 8px; }
.ps-entry-fig + .ps-entry-fig { margin-top: 24px; }
/* Next-scene CTA (replaces hero video in right column) */
.ps-entry-next {
  display: flex; flex-direction: column; gap: 14px;
  padding: 36px 0 28px;
  border-top: 1px solid var(--stone-400);
}
.ps-entry-next-hint {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: #ffffff;
}
.ps-entry-fig .crop { grid-column: 1 / -1; display: block; width: 100%; min-width: 0; height: auto; filter: grayscale(0.25) contrast(1.05); cursor: pointer; }
.ps-entry-fig figcaption { grid-column: 1 / -1; min-width: 0; display: flex; align-items: baseline; font-size: var(--body-base); line-height: 1.45; color: var(--stone-600); margin: 0; container-type: inline-size; }
.ps-entry-fig figcaption .cap-text { flex: 1; min-width: 0; text-align: justify; hyphens: none; -webkit-hyphens: none; overflow-wrap: break-word; }
/* see the identical "no justify under 320px" comment on .ps-brief above */
@container (max-width: 320px) {
  .ps-entry-fig figcaption .cap-text { text-align: left; }
}

/* ---- entry figure viewer (EntryLightbox, Experience.jsx) — click any
   .ps-entry-fig image to open. Portaled to document.body, translucent
   full-screen layer in the same spirit as .nav-drop-scrim (the mobile nav
   menu's own scrim), but black rather than paper-toned: figures are
   photography, and a dark ground is the near-universal convention for
   viewing images large, giving them more contrast to read against than
   the site's own warm parchment tone would. */
.ps-lightbox {
  position: fixed; inset: 0; z-index: 10001;
  background: rgba(0, 0, 0, 0.9);
  display: flex; align-items: center; justify-content: center;
  padding: 70px 100px;
  animation: ps-lightbox-in var(--dur-base) var(--ease-drift) both;
}
@keyframes ps-lightbox-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .ps-lightbox { animation: none; } }

.ps-lightbox-fig { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 18px; max-width: 100%; max-height: 100%; }
/* max-height + max-width together (rather than either alone) is what
   makes every figure read as "similarly sized" regardless of its own
   aspect ratio — a tall figure is bound by max-height, a wide one by
   max-width, so each fills as much of the same bounding box as its shape
   allows instead of showing at wildly different native/inconsistent
   scales. w:2400 (vs. w:1400 for the inline entry thumbnail) requests a
   larger source render, since this view shows the image much bigger. */
.ps-lightbox-fig img { display: block; max-width: 100%; max-height: 76vh; width: auto; height: auto; object-fit: contain; }
.ps-lightbox-fig figcaption {
  font-family: var(--font-body); font-size: var(--body-base); line-height: 1.5;
  color: rgba(255, 255, 255, 0.82); text-align: center; max-width: 640px;
}

.ps-lightbox-close {
  position: fixed; top: 26px; right: 32px;
  background: none; border: 0; padding: 8px; margin: -8px;
  color: #ffffff; cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-base) var(--ease-drift);
}
.ps-lightbox-close:hover { opacity: 0.7; }

.ps-lightbox-nav {
  position: fixed; top: 50%; transform: translateY(-50%);
  background: none; border: 0; padding: 14px; color: #ffffff;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-base) var(--ease-drift);
}
.ps-lightbox-nav:hover { opacity: 0.7; }
.ps-lightbox-prev { left: 14px; }
.ps-lightbox-next { right: 14px; }

@media (hover: none) and (pointer: coarse) {
  .ps-lightbox { padding: 60px 20px; }
  .ps-lightbox-fig img { max-height: 62vh; }
  .ps-lightbox-close { top: 16px; right: 16px; }
  .ps-lightbox-prev { left: 2px; }
  .ps-lightbox-next { right: 2px; }
  .ps-entry + .ps-entry { padding-top: 30px; }
}

/* .ps-entry-text always renders first now (prose read before its figures —
   see renderColumn in Experience.jsx). The 26px that used to separate a
   leading figure from the text below it now separates text from whatever
   figure follows it instead — but only via the adjacent-sibling rule
   below, not as text's own unconditional trailing padding. Giving
   .ps-entry-text a bottom padding regardless of whether a figure follows
   double-counted against .ps-entry + .ps-entry's own 30px gap (and
   .ps-plates-foot's own 26px padding-top) for figure-less entries, making
   them sit noticeably farther from whatever came next than entries ending
   in a figure — confirmed by measuring the live page: 56px between
   figure-less entries vs. 30px between entries ending in a figure. */
.ps-entry-text {
  display: grid; grid-template-columns: 1fr minmax(0, 400px); gap: 24px;
}
.ps-entry-text + .ps-entry-fig { margin-top: 26px; }
.ps-entry-label { display: flex; flex-direction: column; gap: 8px; }
.ps-entry-label .sub { font-family: var(--font-body); font-weight: 400; font-size: var(--body-lg); line-height: 1.3; color: var(--ink); }
.ps-entry-desc { container-type: inline-size; }
.ps-entry-desc .prose { font-size: var(--body-md); line-height: 1.65; color: var(--ink); margin: 0; text-align: justify; hyphens: none; -webkit-hyphens: none; overflow-wrap: break-word; }
.ps-entry-desc ol.prose, .ps-entry-desc ul.prose { padding-left: 1.4em; }
.ps-entry-desc .prose + .prose { margin-top: 0.9em; }
/* editor-added links (see the link annotation, blockContent.ts) — an
   underline by default (not just on hover) since these sit inline in
   flowing justified text with nothing else marking them as clickable,
   unlike a standalone link element that can rely on its own position. */
.ps-entry-desc .prose a { color: inherit; text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--stone-400); transition: color var(--dur-base) var(--ease-drift), text-decoration-color var(--dur-base) var(--ease-drift); }
.ps-entry-desc .prose a:hover { color: #ffffff; text-decoration-color: #ffffff; }
/* see the identical "no justify under 320px" comment on .ps-brief above —
   this column is minmax(0,400px) (see .ps-entry-text), so it can
   genuinely narrow past 320px well before the page itself gets that
   tight. */
@container (max-width: 320px) {
  .ps-entry-desc .prose { text-align: left; }
}

.ps-plates-foot { margin-top: auto; padding-top: 26px; }
.ps-plates-foot .lbl { font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--tracking-eyebrow); text-transform: uppercase; color: var(--stone-600); }
.ps-plates-foot .nm { font-family: var(--font-display); font-size: var(--title-md); color: var(--ink); margin: 6px 0 14px; }

/* shared step-in / return control — style/hover matched to ps-detail-head's
   mark/nav (font-display, weight 300, ink-500 default, white hover) */
.ps-foot-btn {
  font-family: var(--font-display); font-size: 18px; font-weight: 300; letter-spacing: .02em;
  background: none; border: 0; cursor: pointer; color: var(--ink-500);
  display: inline-flex; align-items: center; gap: 10px; margin-top: 8px;
}
.ps-foot-btn.back { margin-top: auto; padding-top: 40px; }
.ps-foot-btn:hover { color: #ffffff; }
.ps-foot-arrow { display: inline-block; transition: transform var(--dur-base) var(--ease-drift); }
.ps-foot-btn:hover .ps-foot-arrow { transform: translateX(4px); }
.ps-foot-btn.back:hover .ps-foot-arrow { transform: translateX(-4px); }

/* .ps-foot-btn--split-only / --stacked-only: "Go home" is duplicated in the
   JSX (Experience.jsx) rather than moved, so each width gets it in the
   right spot without needing JS to know which layout is active. Above
   1000px, the split layout's own "Go home" (end of .ps-leaf) shows and the
   stacked-only duplicate (in .ps-plates-foot) stays hidden; below 1000px
   that's reversed, so it ends up at the true end of the single combined
   column, right next to "Keep unfolding", instead of stranded in the
   middle between the two halves' content. */
.ps-foot-btn--stacked-only { display: none; }
@media (max-width: 1000px) {
  /* .ps-detail itself must stay display:flex/flex-direction:column — that's
     what makes .ps-detail-scroll's flex:1 actually constrain its height to
     the viewport, which is what makes .ps-detail-scroll the one true
     scrolling container (with its scrollbar hidden). Overriding .ps-detail
     to display:block here (an old artifact of this media query, from
     before the touch redesign) broke that: .ps-detail-scroll's flex:1 went
     inert, so it just grew to fit all its content instead of clipping/
     scrolling, and .ps-detail's own inset:0-driven fixed height + its
     unrelated overflow-y:auto became the *real* scroll container instead —
     one with no scrollbar-hiding CSS of its own, which is exactly the
     stray native scrollbar, the "stuck" scroll behavior, and the
     unreachable footer buttons (nothing was actually scrolling the
     .ps-detail-scroll element JS measures against). */
  /* .ps-split-wrap itself was still position:sticky with a hardcoded
     height:100vh — a fixed-height box that doesn't grow for the now-block,
     overflow:visible content stacked inside it. .ps-detail-scroll's
     scrollHeight only ever budgeted 100vh for the whole split section, so
     the real (much taller) stacked content rendered past that budget:
     scrolling felt like it stalled partway through, the footer buttons at
     the very end were unreachable, and the mismatch between visual and
     scrollable height was what surfaced as a stray scrollbar. Resetting it
     to normal flow lets .ps-detail-scroll's own height (and its one hidden
     scrollbar) account for the true, full content height. */
  .ps-split-wrap { position: static; height: auto; }
  .ps-split { display: block; flex: none; min-height: 0; }
  .ps-leaf, .ps-plates { height: auto; min-height: 0; overflow: visible; }
  /* side padding + max-width match About's own content width exactly
     (.pg-wrap's 40px padding, its [data-layout="centered"] 800px
     max-width, and margin:auto centering, see pages.css) rather than the
     8px, uncapped-width box this inherits from desktop's tight two-column
     layout. 40px (not 24px) is the correct default here: About's own 24px
     only kicks in below ITS 860px breakpoint (a separate, narrower
     threshold than this 1000px one) — between 861-999px About is still on
     its desktop 40px padding, so matching this rule to 24px across the
     whole <1000px range left Detail's text column measurably wider than
     About's in that gap. The nested 860px override right below restores
     24px to mirror About's own inner breakpoint exactly. */
  .ps-col-inner { min-height: 0; padding-left: 40px; padding-right: 40px; max-width: 800px; margin-left: auto; margin-right: auto; }
}
/* the 800px cap above is a genuine desktop readability width, but a phone
   rotated to landscape is often already wider than 800px within this same
   max-width:1000px range — the cap then centers the column and leaves
   visible empty margins on both sides instead of using the screen. Touch
   devices always get the full width regardless of orientation; only real
   desktop (and a narrow-but-mouse-driven window) keeps the cap. Mirrors
   the identical .pg-wrap override in pages.css. */
@media (hover: none) and (pointer: coarse) {
  .ps-col-inner { max-width: none !important; }
}
/* mirrors About's own inner breakpoint (.pg-wrap's padding-left/right:24px
   under max-width:860px, see pages.css) — a second, narrower threshold
   nested within the 1000px one above, not a Detail-specific choice. */
@media (max-width: 860px) {
  .ps-col-inner { padding-left: 24px; padding-right: 24px; }
}
@media (max-width: 1000px) {
  /* no divider (border) between the two stacked halves — .ps-plates' own
     --content-top top padding (meant to clear the fixed header, relevant
     only for whichever column is actually first on the page) also has to
     go, or the second column would open with ~98px of dead space before
     its own content, reading as a break even without the border. .ps-leaf
     gets the same treatment for the same reason: on mobile it's no longer
     the first thing on screen (the hero video + brief precede it), so its
     own --content-top clearance is vestigial — left in place it was
     stacking with .ps-split-wrap's margin-top and the brief's own bottom
     padding to read as ~186px of dead space below the brief versus ~32px
     above it. */
  .ps-plates { border-left: 0; border-top: 0; }
  .ps-leaf .ps-col-inner { padding-top: 8px; }
  /* .ps-plates' own gap can't be the same 8px — that was sized to keep the
     brief's above/below spacing balanced (see .ps-leaf above and the
     .ps-hero-pin-stage .ps-brief padding fix earlier in this file), not to
     separate actual content. Once collapsed, .ps-leaf's last entry and
     .ps-plates' first entry sit right next to each other with only 8px
     between them — reading as no space at all rather than "no visible
     divider." Matching the standard .ps-entry + .ps-entry gap (30px) here
     makes this column boundary read exactly like any other entry-to-entry
     gap: still no border/break, but with real breathing room. */
  .ps-plates .ps-col-inner { padding-top: 30px; }
  .ps-foot-btn--split-only { display: none; }
  /* explicit row (rather than relying on the buttons' own inline-flex
     display to flow side by side within .ps-plates-foot's default block
     layout) — "Go home" anchors to the left edge, "Keep unfolding" to the
     right, each sitting the same distance from its own side as the other
     (.ps-col-inner's left/right padding is symmetric), rather than
     clustering in the middle with a fixed gap between them. flex-wrap is
     just a graceful fallback if a narrower phone ever can't fit both.
     padding-top bumped from the base 26px to 48px — that base value reads
     fine on desktop where .ps-plates-foot is pinned to the bottom of a
     full-height pane via margin-top:auto, but here it's sitting right
     after ordinary flowed content with no auto-margin push, so the same
     26px felt like it barely separated the buttons from the last entry. */
  .ps-plates-foot { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; padding-top: 148px; }
  /* .ps-foot-btn.back's own padding-top:40px/margin-top:auto (further up
     this file) are sized for the original "Go home"'s position at the
     bottom of .ps-leaf's own column — this duplicate keeps the .back class
     for its hover-arrow direction, but sits inline next to "Keep
     unfolding" instead, so that spacing has to be reset or it renders
     visibly taller than its neighbor and drops to its own line. Needs the
     full .ps-foot-btn.back.ps-foot-btn--stacked-only chain (3 classes) to
     out-specify .ps-foot-btn.back (2 classes) above — a plain
     .ps-foot-btn--stacked-only override loses to it regardless of source
     order, since specificity beats position in the cascade. */
  .ps-foot-btn--stacked-only { display: inline-flex; }
  .ps-foot-btn.back.ps-foot-btn--stacked-only { margin-top: 0; padding-top: 0; }
  /* smaller than the default 18px — at phone widths "Go home" and "Keep
     unfolding" combined don't fit on one row at the desktop size, and
     wrapping them defeats "next to each other". An explicit height (rather
     than trusting line-height alone) guarantees the two buttons match
     exactly — the &larr;/&rarr; arrow glyphs render with slightly
     different natural line-heights even at line-height:1. */
  /* margin-top:0 here (not just on the .back chain above) — the base
     .ps-foot-btn rule gives every foot button an 8px margin-top, which
     "Keep unfolding" (a plain .ps-foot-btn, unlike "Go home"'s already-
     zeroed .back chain) still had; in this flex row that pushed it a few
     px lower than "Go home" even with equal heights and align-items:center. */
  .ps-plates-foot .ps-foot-btn { font-size: 15px; gap: 8px; line-height: 1; height: 20px; margin-top: 0; }
  .ps-entry-fig { grid-template-columns: 1fr; }
  .ps-entry-fig figcaption { grid-column: 1; }
  .ps-entry-text { grid-template-columns: 1fr; gap: 12px; }
}
