/* ---------- Reset & tokens ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg: #000000;
  --ink: #e2e6e3;
  --ink-soft: #8e8e93;
  --ink-dim: rgba(255, 255, 255, 0.55);
  --card: #1c1c1e;
  --card-hover: #2c2c2e;
  --hairline: #2a2a2c;
  --radius: 10px;
  --radius-card: 14px;
  --img-outline: rgba(255, 255, 255, 0.12);
  --pad: clamp(8px, 1.2vw, 16px);
  --font: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont,
          "Helvetica Neue", Helvetica, Arial, sans-serif;
}

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

body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  height: 100vh;
  height: 100dvh;
  transition: background 0.3s ease, color 0.3s ease;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; color: inherit; background: none; border: 0; cursor: pointer; font-size: 14px; }
a, button, .card { -webkit-tap-highlight-color: transparent; }

/* Controls behave like controls: a long press never selects the page or
   pops the image callout (answer and detail copy stay selectable) */
.roulette, .marquee, .prompt-chip, .card, .q-pill, .overlay-close,
.mode-toggle, .ont-node, .case-pages img, .case-zoom {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

img { -webkit-user-drag: none; -webkit-touch-callout: none; }

/* Keyboard focus: a quiet ring that appears only when navigating by
   keyboard (focus-visible) — mouse clicks stay clean */
a:focus-visible,
button:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.65);
  outline-offset: 2px;
}

.card:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.75);
  outline-offset: 3px;
}

/* ---------- Top bar (menu-bar metaphor) ---------- */
.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px var(--pad);
  /* Above the detail overlay (z 100), so the nav survives everywhere */
  z-index: 110;
}

/* Scroll-aware glass: at the top of the page the bar is literally
   nothing — zero fill, zero blur, one surface with the background.
   The heavy blur fades in only once content passes beneath. */
.topbar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  backdrop-filter: blur(44px);
  -webkit-backdrop-filter: blur(44px);
  opacity: 0;
  transition: opacity 0.35s ease;
}

html.scrolled .topbar::before { opacity: 1; }

.logo { font-size: 14px; font-weight: 400; }

.topbar-right { display: flex; align-items: center; gap: 18px; }

.clock {
  position: relative;
  font-size: 14px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  cursor: default;
}

/* The clock rests quietly until you reach for it */
.clock-live {
  display: inline-block;
  transition: opacity 0.15s ease;
}

.clock-alt {
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
}

.intro-meta:hover .clock-live,
.topbar-right:hover .clock-live {
  opacity: 0;
}

/* 번쩍 — the wake-up: it springs upright past vertical, then settles */
.intro-meta:hover .clock-alt,
.topbar-right:hover .clock-alt {
  animation: wake 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  opacity: 1;
}

@keyframes wake {
  0% { opacity: 0; transform: rotate(-4deg) translateY(3px) scale(0.96); filter: blur(2px); }
  55% { opacity: 1; transform: rotate(1.6deg) translateY(-2px) scale(1.05); filter: none; }
  100% { opacity: 1; transform: none; }
}

/* Each further confession lands with a small pop */
.clock-alt.repop {
  animation: repop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes repop {
  40% { transform: scale(1.06) translateY(-1px); }
}

.intro-contact,
.topbar-link {
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
}

.intro-meta:hover .intro-contact,
.topbar-right:hover .topbar-link {
  transform: scale(1.14);
}

/* Hold long enough and the invitation gets serious */
.intro-meta.really .intro-contact,
.topbar-right.really .topbar-link {
  transform: scale(1.3);
}

.topbar-link { font-size: 14px; }
.topbar-link:hover { opacity: 0.6; }

/* ---------- Feed ----------
   The page scrolls inside this container, so the rubber-band bounce
   happens here while the fixed topbar stays completely still. */
.feed {
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-behavior: smooth;
  padding: 68px var(--pad) 40px;
}

/* ---------- Ask section ---------- */
.ask { padding-top: clamp(4px, 1.6vh, 20px); }

/* ---------- Landing (the initial home): a vertical reel of questions ----------
   The queries drift upward past an arrow indicator; whichever crosses
   the arrow lights up. Pressing one drops into the conversational home. */
/* The landing fills the viewport and its content sits dead-center —
   equal top and bottom margins at any screen size */
html.landing .feed {
  padding-top: 0;
  padding-bottom: 0;
}

html.landing .ask {
  display: grid;
  grid-template-columns: minmax(280px, 5fr) 7fr;
  align-items: center;
  align-content: center;
  gap: clamp(24px, 5vw, 90px);
  padding-top: 0;
  min-height: 100vh;
  min-height: 100dvh;
}

.landing-intro { display: none; }

/* Coming home: the intro and the reel re-enter softly, not by a reload */
html.landing.landing-in .landing-intro,
html.landing.landing-in .roulette {
  animation: land-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes land-in {
  from { opacity: 0; transform: translateY(12px); }
}

html.landing .landing-intro {
  display: block;
  justify-self: end;
  padding-right: clamp(28px, 6vw, 110px);
}

.landing-intro h1 {
  font-size: 14px;
  font-weight: 400;
  line-height: 1.45;
}

.intro-role {
  margin-top: 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink-soft);
  white-space: nowrap;
}

.intro-meta {
  margin-top: 12px;
  padding: 8px 0;
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 14px;
  color: var(--ink-dim);
}

.intro-meta .clock { font-size: 14px; }

.intro-contact { color: var(--ink); }
.intro-contact:hover { opacity: 0.6; }


html.landing .topbar { display: none; }
html.landing .ask-head { display: none; }
html.landing .chat-log { display: none; }
html.landing .marquee { display: none; }
html.landing .work { display: none; }
html.landing .feed-foot { display: none; }

.roulette { display: none; }

html.landing .roulette {
  display: block;
  position: relative;
  height: min(62vh, 600px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 22%, black 78%, transparent);
  mask-image: linear-gradient(to bottom, transparent, black 22%, black 78%, transparent);
}

.roulette-ind {
  position: absolute;
  left: calc(50% - min(232px, 38vw));
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  color: var(--ink-soft);
  z-index: 2;
  pointer-events: none;
}

/* A quick nudge each time a new question locks onto the arrow */
.roulette-ind.tick {
  animation: ind-nudge 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes ind-nudge {
  0% { transform: translateY(-50%) translateX(0); }
  35% { transform: translateY(-50%) translateX(5px); }
  100% { transform: translateY(-50%) translateX(0); }
}

.roulette-track {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  will-change: transform;
}

.roulette .prompt-chip {
  font-size: 14px;
  padding: 10px 18px;
  transition: background 0.22s ease, color 0.22s ease,
    transform 0.34s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.roulette .prompt-chip.hot {
  background: var(--ink);
  color: var(--bg);
  transform: scale(1.07);
}

.ask-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 14px;
}

.ask-label {
  font-size: 14px;
  color: var(--ink-soft);
}

/* Grid ⇄ Ontology segmented toggle — the thumb glides between segments */
.mode-toggle {
  position: relative;
  display: flex;
  gap: 2px;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  padding: 3px;
}

.mode-thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  border-radius: 999px;
  background: var(--ink);
  transition:
    left 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    width 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}

.mode-btn {
  position: relative;
  z-index: 1;
  padding: 6px 14px;
  border-radius: 999px;
  color: var(--ink-soft);
  transition: color 0.25s ease;
}

.mode-btn.active { color: var(--bg); }

/* ---------- Ontology (Palantir-benchmarked) ----------
   A quiet, precise object graph: dot-grid canvas, compact typed nodes,
   thin static links. Queries light matched paths in Blueprint blue. */
.ontology {
  position: relative;
  height: clamp(680px, 74vw, 880px);
  background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
  background-size: 24px 24px;
  border-radius: var(--radius-card);
}

.ont-edges {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.ont-edge {
  fill: none;
  stroke: rgba(255, 255, 255, 0.13);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  transition: stroke 0.3s ease;
}

.ont-edge.attr { stroke-dasharray: 3 4; stroke: rgba(255, 255, 255, 0.08); }

/* Object nodes */
.ont-node {
  position: absolute;
  left: var(--x);
  top: var(--y);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 11px;
  border-radius: 6px;
  background: #17171a;
  border: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
  transition: opacity 0.3s ease, border-color 0.3s ease, background 0.2s ease;
}

.ont-node:hover { background: #202024; }

.ont-node .dot {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--dot, #8e8e93);
  flex: none;
}

.ont-node .count { color: var(--ink-soft); font-size: 12px; }
.ont-node .sub { color: var(--ink-soft); font-size: 12px; }

/* Long project titles stay on one line and truncate before the next column */
.ont-node.project { max-width: 22%; }
.ont-node .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ont-node.root { padding: 9px 14px; font-size: 14px; }

.ont-node.tag { border-style: dashed; }

/* Electric sparks: a short bright segment racing along an edge */
.ont-edge.comet {
  stroke: #a9c9ff;
  stroke-width: 2;
  transition: none;
}

/* Entry nodes charge up while the agent is thinking, then discharge */
.ont-node.charged {
  border-color: #4786e0;
  animation: charge 0.9s ease-in-out infinite;
}

@keyframes charge {
  50% { box-shadow: 0 0 14px rgba(71, 134, 224, 0.4); }
}

/* A brief synaptic flash when a node receives the signal */
.ontology.filtered .ont-node.lit {
  animation: synapse 0.6s ease-out;
  animation-delay: inherit;
}

@keyframes synapse {
  0% { box-shadow: 0 0 0 0 rgba(110, 166, 255, 0.55); }
  100% { box-shadow: 0 0 0 10px rgba(110, 166, 255, 0); }
}

/* Query highlighting — Blueprint blue accents, everything else recedes */
.ontology.filtered .ont-node { opacity: 0.3; }
.ontology.filtered .ont-node.lit { opacity: 1; border-color: #4786e0; }
.ontology.filtered .ont-edge { stroke: rgba(255, 255, 255, 0.04); }
.ontology.filtered .ont-edge.lit { stroke: rgba(71, 134, 224, 0.75); }

/* Hover focus: a node and its direct neighborhood */
.ontology.hovering .ont-node:not(.hov) { opacity: 0.3; }
.ontology.hovering .ont-node.hov { opacity: 1; }
.ontology.hovering .ont-edge:not(.hov) { stroke: rgba(255, 255, 255, 0.04); }
.ontology.hovering .ont-edge.hov { stroke: rgba(255, 255, 255, 0.5); }

/* Small thumbnail peek when hovering a project node */
.ont-preview {
  position: absolute;
  z-index: 5;
  width: 200px;
  aspect-ratio: 16 / 9;
  border-radius: 8px;
  overflow: hidden;
  pointer-events: none;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
}

.ont-preview img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---------- Ambient lighting (colors sampled from visible work) ---------- */
@property --blob-c {
  syntax: "<color>";
  inherits: false;
  initial-value: #5a6478;
}

:root {
  /* Dither: a tiled fractal-noise texture laid over long dark gradients
     at a few percent opacity. Browsers draw gradients in 8-bit with no
     dithering, so on low-gamut screens and projectors the bands show;
     a whisper of noise breaks them up without being visible itself. */
  --dither: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

.ambient {
  position: fixed;
  inset: -12%;
  z-index: -1;
  pointer-events: none;
}

.ambient::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--dither);
  background-size: 180px 180px;
  opacity: 0.045;
  mix-blend-mode: overlay;
}

/* Soft light without a filter: the falloff is baked into the gradient,
   so animating the blob costs almost nothing on the GPU */
@property --blob-c {
  syntax: "<color>";
  inherits: false;
  initial-value: transparent;
}

.blob {
  transition: --blob-c 1.4s ease;
  position: absolute;
  width: 110vw;
  height: 110vw;
  border-radius: 50%;
  opacity: 0.42;
  background: radial-gradient(closest-side, var(--blob-c), transparent 72%);
  transition: --blob-c 2.4s ease;
  will-change: transform;
}

/* The lights drift only while a response is generating (:root.gen) —
   when idle the background is static, so every glass surface above it
   can be cached and the GPU goes quiet. */
.blob:nth-child(1) { top: -36%; left: -24%; animation: drift-a 26s ease-in-out infinite alternate paused; }
.blob:nth-child(2) { top: 8%; right: -32%; animation: drift-b 34s ease-in-out infinite alternate paused; }
.blob:nth-child(3) { bottom: -44%; left: 8%; animation: drift-c 30s ease-in-out infinite alternate paused; }

:root.gen .blob,
:root.warp .blob { animation-play-state: running; }

@keyframes drift-a { to { transform: translate(10vw, 9vh) scale(1.18); } }
@keyframes drift-b { to { transform: translate(-9vw, -7vh) scale(0.9); } }
@keyframes drift-c { to { transform: translate(7vw, -10vh) scale(1.12); } }

/* Portrait screens (phones, tablets upright): the lights are sized by the
   long side and centred where they sit on a desktop screen — upper left,
   lower right, lower middle, in viewport fractions — so a phone gets the
   same wide, overlapping wash instead of one small glow at the edge.
   (On a 1440 × 900 desktop the three centres land at about (13%, 31%),
   (97%, 86%) and (53%, 79%) with a 55vw radius.) The .ambient box starts
   12% outside the viewport, hence the +12 offsets. */
@media (max-aspect-ratio: 1/1) {
  .blob {
    width: 110vmax;
    height: 110vmax;
    right: auto;
    bottom: auto;
  }

  .blob:nth-child(1) { top: calc(43vh - 55vmax); left: calc(25vw - 55vmax); }
  .blob:nth-child(2) { top: calc(98vh - 55vmax); left: calc(109vw - 55vmax); }
  .blob:nth-child(3) { top: calc(91vh - 55vmax); left: calc(65vw - 55vmax); }
}

.marquee {
  overflow: hidden;
  white-space: nowrap;
  padding: 5px 0;
  margin: 0 calc(var(--pad) * -1);
}

/* Tracks are driven from JS with eased velocity, so hovering decelerates
   smoothly in both directions instead of hard-pausing (which can snap) */
.marquee-track {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding-right: 10px;
  will-change: transform;
}

/* iMessage-suggestion-style prompt pills. No backdrop-filter here: these move
   constantly, and sampling the backdrop every frame is what heats the GPU. */
.prompt-chip {
  font-size: 14px;
  font-weight: 400;
  padding: 10px 18px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease, opacity 0.35s ease;
}

.prompt-chip:hover { background: rgba(255, 255, 255, 0.2); transform: scale(0.97); }
.prompt-chip.active { background: var(--ink); color: var(--bg); }

/* ---------- Chat log ---------- */
.chat-log {
  display: flex;
  flex-direction: column;
  padding-bottom: 10px;
  /* JS pins the height in px while streaming; new lines ease in */
  overflow: hidden;
  transition: height 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.exchange { display: flex; flex-direction: column; }

.assistant { position: relative; z-index: 0; }

.ask-answer {
  padding: 16px 0 4px;
  font-size: 28px;
  font-weight: 400;
  line-height: 1.25;
  min-height: 1.4em;
  max-width: 30em;
}

/* Streamed chunks fade in like tokens */
.tok { opacity: 0; animation: tok-in 0.35s ease forwards; }
/* Keywords fall out of the query pill above them — decomposition made visible */
@keyframes tok-in {
  from { opacity: 0; transform: translateY(-14px) scale(0.9); }
  to { opacity: 1; transform: none; }
}

/* Agent work log shown while the response is being prepared —
   answer-sized, muted color, set in a coding face */
.agent-log {
  padding: 16px 0 4px;
  font-size: 16px;
  line-height: 1.25;
  font-family: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  color: var(--ink-soft);
  animation: log-pulse 1.1s ease-in-out infinite;
}

@keyframes log-pulse { 50% { opacity: 0.45; } }

.caret {
  display: inline-block;
  width: 0.5em;
  height: 1.05em;
  margin-left: 0.1em;
  background: var(--ink);
  vertical-align: text-bottom;
  animation: blink 1s steps(1) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

/* Inline project mentions — hover highlights, click opens the project */
.mention {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--ink-soft);
  text-underline-offset: 4px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.mention:hover { background: var(--ink); color: var(--bg); text-decoration-color: transparent; }

/* Card echo while its mention is hovered */
.card.hilite { box-shadow: 0 0 0 2px var(--ink); }

.answer-meta {
  font-size: 12px;
  color: var(--ink-soft);
  margin: 8px 0 6px;
}

/* ---------- Project grid ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--pad); /* gutter = page side margin */
}

.card {
  position: relative;
  text-align: left;
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-card);
  overflow: hidden;
}

.card.off { display: none; }

.card-img {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

/* Layer stack inside each card: scene / type / subject, hidden until the
   card is hovered or the scan judgement pulls the image apart */
.card-layers {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}

/* 3-layer cards have no baked thumbnail beneath — the stack IS the card */
.card-layers.composed { opacity: 1; }

/* Gradient-backed cards: the same key-color radial the detail hero uses,
   oversized so the hover parallax never exposes an edge */
.lay-bg.grad {
  position: absolute;
  left: -10%;
  top: -10%;
  width: 120%;
  height: 120%;
  background:
    radial-gradient(90% 70% at 50% 104% in oklab,
      color-mix(in srgb, var(--card-glow, #3a3f4a) 42%, var(--card-base, #0b0b0d)) 0%,
      color-mix(in srgb, var(--card-glow, #3a3f4a) 26%, var(--card-base, #0b0b0d)) 22%,
      color-mix(in srgb, var(--card-glow, #3a3f4a) 14%, var(--card-base, #0b0b0d)) 45%,
      color-mix(in srgb, var(--card-glow, #3a3f4a) 5%, var(--card-base, #0b0b0d)) 72%,
      var(--card-base, #0b0b0d) 100%);
}

.lay-bg.grad::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--dither);
  background-size: 180px 180px;
  opacity: 0.06;
  mix-blend-mode: overlay;
}

/* Beats the generic .card-img img fade-in rule — layer images are only
   revealed by their container, never individually faded */
.card-img .card-layers img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 1;
  transition: none;
}

.card:hover .card-layers { opacity: 1; }
.card.decomp .card-layers { opacity: 1; transition: none; }

/* The baked photo stays fully opaque beneath the stack — the 3-layer
   stack covers it completely (identical pixels), so fading only the
   stack above it can never dip to the card background */

/* Hover: the card parallaxes like the hero — the scene recedes from the
   mouse, the subject follows it, the type floats closest. Amplitudes are
   tiny and the ease is slow, so it reads as a feeling, not an event. */
/* Center-origin scale gives every edge of the subject's canvas a margin
   (~1.6px vertical, ~2.9px horizontal at card size) that exceeds the
   parallax travel, so no crop line can ever peek in from any side */
.card-img .card-layers img {
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

.card:hover .card-layers .lay-bg {
  transform: translate(calc(var(--cx, 0) * -3px * var(--par, 1)), calc(var(--cy, 0) * -2px * var(--par, 1))) scale(1.02);
}

.card:hover .card-layers .lay-iso {
  transform: translate(calc(var(--cx, 0) * 3px * var(--par, 1)), calc(var(--cy, 0) * 2px * var(--par, 1))) scale(1.018);
}

.card:hover .card-layers .lay-type {
  transform: translate(calc(var(--cx, 0) * 6px * var(--par, 1)), calc(var(--cy, 0) * 4px * var(--par, 1)));
}

/* The two print works hover as a single photograph: their posters are
   physically part of the wall, and a layer split risks losing the
   background — so no layer stack at all */
.card[data-id="fundamentals-from-swiss"] .card-layers,
.card[data-id="residential-density"] .card-layers { display: none; }

/* Subjects that live inside their scene (exhibition walls, posters, a
   silhouette in a room) separate awkwardly — damp their parallax */
.card[data-id="the-designer-s-role-in-the-age-of-ai"],
.card[data-id="fundamentals-from-swiss"],
.card[data-id="residential-density"] {
  --par: 0.4;
}

/* Scan judgement: a matched record briefly splits into its layers */
.card.decomp .card-layers .lay-iso {
  animation: decomp-iso 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.card.decomp .card-layers .lay-type {
  animation: decomp-type 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes decomp-iso {
  0%   { transform: none; }
  40%  { transform: scale(1.04); filter: drop-shadow(0 10px 12px rgba(0, 0, 0, 0.6)); }
  100% { transform: none; filter: none; }
}

@keyframes decomp-type {
  0%   { transform: none; opacity: 1; }
  40%  { transform: translateY(5px) scale(0.98); opacity: 0.85; }
  100% { transform: none; opacity: 1; }
}

.card-img img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.card-img img.loaded { opacity: 1; }


/* View-transition timing: soft, spring-like glide between grid layouts */
::view-transition-group(*) {
  animation-duration: 0.52s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}

/* Shared-element morphs between boxes of different aspect (a 16:9 card
   growing into the ~2.6:1 hero band): each snapshot fills the animating
   box and crops, instead of keeping its own height and spilling past it */
::view-transition-image-pair(*) { overflow: clip; }

::view-transition-old(*),
::view-transition-new(*) {
  height: 100%;
  object-fit: cover;
}

::view-transition-old(root),
::view-transition-new(root) { object-fit: fill; }

/* Citation jump highlight */
.card.flash { animation: card-flash 1.6s ease; }
@keyframes card-flash { 20% { box-shadow: 0 0 0 2px var(--ink); } }


/* Meta strip below the full thumbnail — glassmorphism: frosted translucent
   layer with backdrop blur over the ambient background */
/* Glass strip that amplifies the ambient light behind the card — the
   blur pulls the blob colors through and pushes their saturation up */
.card-meta {
  padding: 10px 14px 12px;
  background: rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(24px) saturate(220%) brightness(1.4);
  -webkit-backdrop-filter: blur(24px) saturate(220%) brightness(1.4);
}

.meta-line { font-size: 14px; color: var(--ink-soft); margin-top: 0; line-height: 1.3; }

/* Title leads the hierarchy by weight; hover confirms the card opens */
.meta-line.is-primary {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  transition: color 0.25s ease;
}

.card:hover .meta-line.is-primary { color: #ffffff; }

/* Tags render as plain text; multiple tags separated by a middle dot */
.meta-line.chip {
  font-size: 14px;
  color: var(--ink-dim);
}

.card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 0;
  line-height: 1.2;
}

.card-tags .chip:not(:first-child)::before {
  content: "·";
  margin-right: 6px;
}

/* Layout variants — each prompt reshapes the grid */
/* Layout variants: 3-across or 2-across — never a full-width single card */
.grid.uniform .card { grid-column: span 2; }
.grid.duo     .card { grid-column: span 3; }
.grid.feature .card { grid-column: span 3; }

/* Scan stage: the whole database lifts into a centered 5 × 3 overlay —
   dead-center on any monitor — while the prompt is narrowed to keywords
   and the matching records light up before being pulled to the main view */
.grid.scan {
  position: fixed;
  inset: 0;
  z-index: 70;
  animation: scan-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
  /* Near-opaque + blur so the streaming answer text never reads through
     the scan stage */
  background: rgba(0, 0, 0, 0.93);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  display: grid;
  grid-template-columns: repeat(5, min(15vw, 176px));
  gap: 12px;
  place-content: center;
  padding: 0 4vw;
  pointer-events: none;
}

.grid.scan .card {
  grid-column: span 1;
  transition: opacity 0.45s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* A record culled by the current keyword sinks back into the dark */
.grid.scan .card.sunk {
  opacity: 0.13;
  transform: scale(0.94);
}

/* Survivors of the active filter wear its color while the wave runs */
.grid.scan .card.lit-wave {
  outline: 2px solid var(--wave-c, #4786e0);
  outline-offset: 2px;
  transition: opacity 0.45s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), outline-color 0.35s ease;
}

/* Corner radius scales with card size — the 2-column (duo) look is the
   reference ratio (14px on a ~620px card ≈ 2.3% of width). Smaller
   cards take visually equivalent, not nominal, radii. */
.grid.uniform .card { border-radius: 9px; }
.grid.scan .card { border-radius: 4px; }

.grid.scan .card-meta { display: none; }

/* The scan is a static contact sheet: no parallax, and any in-flight
   hover transform/opacity from the big grid snaps to rest instantly —
   nothing may drift while the records are being read. (The judgement's
   decomp animation still runs; it is animation-driven, not hover-driven.) */
.grid.scan .card-img .card-layers img,
.grid.scan .card-img .card-layers .lay-bg {
  transition: none;
  transform: none;
}

.grid.scan .card-layers { transition: none; }

/* The query on stage: the pressed chip itself, landed above the keywords */
.scan-query { display: none; }

.q-pill {
  max-width: min(760px, 88vw);
  font-size: 14px;
  padding: 10px 18px;
  border-radius: 999px;
  /* Dark from the very first frame — pressing a query IS the mode switch */
  background: #17171a;
  color: var(--ink);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
  text-align: center;
  line-height: 1.9;
}

/* The pill parsed in place: fixed where the chip (or search field) sat */
.q-pill.stage {
  position: fixed;
  z-index: 90;
  transform: translateX(-50%);
  white-space: nowrap;
  line-height: normal;
  pointer-events: none;
  /* The content sits centered in one inner box; while the pill eases to
     its next size the box keeps its final layout, so the edges open and
     close around it symmetrically and nothing reflows mid-way */
  display: flex;
  justify-content: center;
  align-items: flex-start;
  /* Clip while the size eases, so nothing pokes past the pill's edge */
  overflow: hidden;
  perspective: 420px;
  animation: pill-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
  transition: opacity 0.3s ease,
    left 0.65s cubic-bezier(0.22, 1, 0.36, 1),
    top 0.65s cubic-bezier(0.22, 1, 0.36, 1),
    width 0.4s cubic-bezier(0.22, 1, 0.36, 1),
    height 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

/* At rest the box fits the pill (and wraps on phones); pillResize pins it
   to the final width only while the pill is easing */
.q-pill .q-inner { display: block; flex: 0 1 auto; min-width: 0; }

.q-pill.stage.centered { left: 50%; top: 16vh; }

.q-pill.stage.out { opacity: 0; }

@keyframes pill-in {
  from { opacity: 0; transform: translateX(-50%) scale(0.96); }
  to { transform: translateX(-50%) scale(1); }
}

/* A natural word, mid-parse; once parsed, the leftovers recede. Words
   keep one box model from rest to flip, so a flip never nudges the line */
.q-pill .q-word { display: inline-block; transition: opacity 0.45s ease; }

/* Beat 1 of a replacement: the source words flip away like a split-flap */
.q-pill .q-word.flipping-out {
  display: inline-block;
  animation: flip-out 0.22s cubic-bezier(0.5, 0, 0.75, 0) both;
}

@keyframes flip-out {
  to { transform: rotateX(88deg); opacity: 0.15; }
}
.q-pill.parsed .q-word,
.q-pill.parsed .q-space { opacity: 0.3; }

/* A system term, crystallized in place of the words it came from.
   Each kind wears its own color: type=blue, category=amber, topic=teal,
   client=blue, keyword=neutral */
.q-key {
  --kc: #7fa8ff;
  display: inline-block;
  font-family: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--kc);
  background: color-mix(in srgb, var(--kc) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--kc) 55%, transparent);
  border-radius: 6px;
  padding: 1px 8px;
  margin: 0 3px;
  line-height: 1.6;
  vertical-align: baseline;
  transition: color 0.3s ease, background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
  animation: flip-in 0.34s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.q-key[data-kind="category"] { --kc: #ffb35c; }
.q-key[data-kind="topic"] { --kc: #5fd4a8; }
.q-key[data-kind="client"] { --kc: #5b9bff; }
.q-key[data-kind="keyword"] { --kc: #c9c9d4; }

/* The term whose wave is currently scanning the records */
.q-key.lit {
  color: color-mix(in srgb, var(--kc) 55%, white);
  border-color: var(--kc);
  background: color-mix(in srgb, var(--kc) 30%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--kc) 45%, transparent);
}

.q-key em {
  font-style: normal;
  font-size: 10px;
  color: color-mix(in srgb, var(--kc) 62%, transparent);
  margin-right: 6px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

@keyframes flip-in {
  from { opacity: 0; transform: rotateX(-88deg); }
}

/* While a query is being worked, the rest of the marquee recedes */
html.asking .marquee .prompt-chip { opacity: 0.18; }

/* Grid grammar: the instant a prompt is pressed, the stage goes black —
   the same dark the scan sheet lives on — and only the pill stays lit */
.grid .card { transition: opacity 0.35s ease; }

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(0, 0, 0, 0.93);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.16s ease;
}

html.staged body::after { opacity: 1; }

/* While a query is being judged, the reel can't start another one */
html.asking .roulette { pointer-events: none; }

/* Landing grammar: the other questions, the intro, and the arrow recede
   while the pressed one parses in place */
html.asking .roulette .prompt-chip {
  opacity: 0.15;
  transform: none;
  background: rgba(255, 255, 255, 0.12);
  color: var(--ink);
}

html.asking .landing-intro {
  opacity: 0.25;
}

.landing-intro { transition: opacity 0.4s ease; }

html.asking .roulette-ind { opacity: 0; }
.roulette-ind { transition: opacity 0.3s ease; }

/* Hand-scrolling the reel: the arrow stands down until the spin settles */
.roulette.scrolling .roulette-ind { opacity: 0.2; }

/* ---------- Landing → scan ----------
   Same grammar as the grid: the stage goes black, the pressed question
   glides to the top center, the record sheet assembles beneath it. The
   work section must render during the ask — the sheet lives in it (the
   landing normally hides it). */
html.landing:is(.asking, .staged) .work { display: block; }

/* The sheet doesn't slam in as a panel — the records assemble one by
   one, in display order */
.grid.scan .card {
  animation: card-pop 0.55s cubic-bezier(0.22, 1, 0.36, 1) backwards;
  animation-delay: calc(0.18s + var(--i, 0) * 28ms);
}

@keyframes card-pop {
  from { opacity: 0; transform: translateY(12px) scale(0.97); }
}

@keyframes scan-in {
  from { opacity: 0; }
}


/* Extracted keywords row — retired; parsing now happens inside the pill */
.scan-keys { display: none; }

/* The reasoning trace, printed on the sheet itself: a fixed-height
   console above the records (so new lines never shove the cards), the
   newest thought brightest and shimmering while it's still open */
.scan-think { display: none; }

.grid.scan .scan-think {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 6px;
  height: 104px;
  margin-bottom: 22px;
  overflow: hidden;
  font-size: 13px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.5);
}

.think-line {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 9px;
  margin: 0;
  opacity: 0.45;
  transition: opacity 0.45s ease, color 0.45s ease;
  animation: think-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.think-line::before {
  content: "";
  flex: none;
  margin-top: calc(0.7em - 2.5px);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
}

.think-line.live,
.think-line.final {
  opacity: 1;
  color: #fff;
}

.think-line.live::before {
  background: #fff;
  animation: think-pulse 1.1s ease-in-out infinite;
}

.think-line.final::before { background: #fff; }

/* An open thought: light sweeps through the words while the model reads */
.think-line.live .th-text {
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.5) 0%, #fff 45%, rgba(255, 255, 255, 0.5) 90%);
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: think-shimmer 1.6s linear infinite;
}

.th-key {
  --kc: #c9c9d4;
  font-family: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--kc);
}

.th-key em {
  font-style: normal;
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-right: 5px;
  opacity: 0.7;
}

.think-line .th-text { flex: 0 1 auto; min-width: 0; }

.th-n {
  flex: none;
  white-space: nowrap;
  color: #fff;
  font-variant-numeric: tabular-nums;
  animation: think-in 0.35s ease both;
}

.th-n::before { content: "— "; color: rgba(255, 255, 255, 0.4); }

@keyframes think-in {
  from { opacity: 0; transform: translateY(7px); }
}

@keyframes think-pulse {
  50% { opacity: 0.25; transform: scale(0.7); }
}

@keyframes think-shimmer {
  from { background-position: 120% 0; }
  to { background-position: -120% 0; }
}

.feed-foot {
  padding-top: 56px;
  font-size: 12px;
  color: var(--ink-soft);
}

/* ---------- Overlay ---------- */
/* Full-screen detail page: the nav keeps its own strip, tinted with the
   average of the artwork's top edge so the two read as one surface */
.overlay {
  position: fixed;
  inset: 0;
  background: var(--bg);
  z-index: 100;
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

/* The detail page carries its own nav in the document flow, glued to the
   hero — pull-down bounces move them together. The global fixed topbar
   steps aside while the detail is open. */
body:has(.overlay.open) .topbar { display: none; }

.overlay-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px var(--pad);
  background: var(--nav-c, #0b0b0d);
  transition: background 0.4s ease;
}

.overlay.open { opacity: 1; pointer-events: auto; }

/* Pinned to the bottom corner, clear of the hero artwork */
.overlay-close {
  position: fixed;
  bottom: calc(24px + env(safe-area-inset-bottom, 0px)); right: var(--pad);
  z-index: 101;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--close-bg, var(--card));
}
.overlay-close:hover { background: var(--card-hover); }

/* The first fold owns the whole viewport on any aspect ratio: nav +
   hero + description stretch to the bottom edge, so the case study
   never shows on arrival — it starts exactly one scroll below */
.overlay-fold {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

.overlay-inner {
  flex: 1 0 auto;
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding: 48px var(--pad) 96px;
}

/* Display title: larger than the 14px body, everything else stays 14px */
.overlay-title {
  font-size: 20px;
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 500;
  margin-bottom: 10px;
}

.overlay-summary {
  color: var(--ink-soft);
  max-width: 44em;
  margin-bottom: 28px;
}

.overlay-info {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 28px;
}

.info-row { display: flex; gap: 20px; }

.info-label {
  flex: none;
  width: 64px;
  color: var(--ink-soft);
}

.info-value { max-width: 44em; }

/* The case study: from the first page down, the detail page IS the
   artwork's white field — full-bleed white, the square pages stacked
   edge to edge in the content column so the sliced panel reads as one
   continuous sheet. Each page reserves its square before it loads, so
   lazy loading never shifts the layout. */
.overlay-case {
  background: #fff;
  padding: 0 var(--pad);
}

.overlay-fold:has(+ .overlay-case:not([hidden])) .overlay-inner { padding-bottom: 48px; }

.case-pages {
  display: flex;
  flex-direction: column;
  max-width: 1100px;
  margin: 0 auto;
}

.case-pages img {
  display: block;
  width: 100%;
  height: auto;
  /* square placeholder until the page loads; the natural ratio wins
     after (the last slice of a panel can be shorter) */
  aspect-ratio: auto 1 / 1;
}

/* Full-bleed hero, Apple-style: a near-solid gradient tinted with the
   project's own key color, the display type behind, and only the subject
   cutout in front. No scene photo — the cutout floats on light. */
.overlay-hero {
  /* A low, full-bleed cinematic band (~2.6:1): the KV spans edge to edge
     and the band crops it vertically, anchored to its bottom */
  /* The band may never push the description out of the first fold: it
     is capped at the viewport minus the nav (44px) and the description
     block (~300px incl. its margins), with a little slack — so on any aspect ratio the
     arrival view is hero + description, and the case study waits one
     scroll below. (Ultrawide screens crop the band a little.) */
  --hero-cap: max(300px, calc(100svh - 350px));
  --hero-h: min(clamp(400px, 38vw, 78vh), var(--hero-cap));
  position: relative;
  width: 100%;
  height: var(--hero-h);
  overflow: hidden;
  background: #0b0b0d;
}

/* With dedicated PDP art the band takes the artwork's exact ratio, so
   the file shows edge to edge with nothing cropped */
.overlay-hero.pdp {
  height: auto;
  aspect-ratio: 5687 / 2160;
  max-height: var(--hero-cap);
}

/* Scene projects: the leftover band width is the scene itself, blurred
   out into ambience behind the sharp KV */
.hero-ambient {
  position: absolute;
  left: -6%;
  top: -6%;
  width: 112%;
  height: 112%;
  object-fit: cover;
  filter: blur(56px) brightness(0.5) saturate(1.1);
  z-index: 1;
  pointer-events: none;
}

.hero-ambient[hidden] { display: none; }

/* PDP-dedicated wide hero art (~2.63:1, made for this exact band):
   shown 1:1 with no crop — the band's ratio matches the artwork's */
.hero-wide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
  pointer-events: none;
}

.hero-wide[hidden] { display: none; }

/* The subject, cut from the PDP art itself, floats above it in perfect
   register — the only moving plane, so the artwork never gets cropped */
.hero-wide-iso {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 2;
  pointer-events: none;
}

.hero-wide-iso[hidden] { display: none; }

/* Sequential wide-canvas pieces (Ark): each one materializes over the
   "off" scene in order, building the AR workspace piece by piece */
.hero-wide-frames {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.hero-wide-frames img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* --fx: persistent slot shift (e.g. swapped panels) — must hold after
     the entrance animation ends */
  transform: translateX(var(--fx, 0px));
  pointer-events: none;
}

.overlay-hero.enter-depth .hero-wide-frames img {
  animation: frame-enter 0.5s ease both;
}

/* The key-color glow rises from behind the subject */
.hero-gradient {
  position: absolute;
  left: -10%;
  top: -10%;
  width: 120%;
  height: 120%;
  background:
    radial-gradient(90% 70% at var(--glow-pos, 50% 104%) in oklab,
      color-mix(in srgb, var(--hero-c, #3a3f4a) 42%, var(--hero-base, #0b0b0d)) 0%,
      color-mix(in srgb, var(--hero-c, #3a3f4a) 26%, var(--hero-base, #0b0b0d)) 22%,
      color-mix(in srgb, var(--hero-c, #3a3f4a) 14%, var(--hero-base, #0b0b0d)) 45%,
      color-mix(in srgb, var(--hero-c, #3a3f4a) 5%, var(--hero-base, #0b0b0d)) 72%,
      var(--hero-base, #0b0b0d) 100%);
  pointer-events: none;
}

.hero-gradient::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--dither);
  background-size: 180px 180px;
  opacity: 0.06;
  mix-blend-mode: overlay;
}

/* The KV stage: the full 16:9 canvas, uncropped, exactly as tall as the
   band and centered — the band's leftover width is just background */
.hero-stage {
  position: absolute;
  left: 50%;
  top: 0;
  height: 100%;
  aspect-ratio: 16 / 9;
  transform: translateX(-50%);
  z-index: 2;
  pointer-events: none;
}

.hero-stage .hero-scene,
.hero-stage .overlay-iso,
.hero-stage .overlay-frames,
.hero-stage .hero-type,
.hero-stage .hero-type img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hero-stage .hero-scene,
.hero-stage .overlay-iso,
.hero-stage .hero-type img {
  object-fit: cover;
  object-position: var(--kv-pos, 50% 100%);
}

.hero-scene[hidden] { display: none; }

/* Stacking inside the stage: scene photo < subject < type on top */
.hero-scene { z-index: 0; }
.hero-stage .overlay-iso { z-index: 1; }
.hero-stage .overlay-frames { z-index: 1; }
.hero-type { z-index: 2; }

/* Frame-split subjects (Wearable, Ark): each frame materializes in
   sequence — per-frame delays are set inline from JS */
.overlay-frames img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--kv-pos, 50% 100%);
  /* --fx spreads side panels out to the bleed so their cuts never show */
  transform: translateX(var(--fx, 0px));
  pointer-events: none;
}

.overlay-hero.enter-depth .overlay-frames img {
  animation: frame-enter 0.45s ease both;
}

/* A centered panel can't push its cuts to the bleed — its sides melt
   into the background instead. The panel's cut lines sit at the canvas
   thirds, so the fade brackets those. */
.overlay-frames img.fade-sides {
  -webkit-mask-image: linear-gradient(90deg,
    transparent 29%, #000 42%, #000 58%, transparent 71%);
  mask-image: linear-gradient(90deg,
    transparent 29%, #000 42%, #000 58%, transparent 71%);
}

/* --fex: entrance-only horizontal offset (slides in from a side cut);
   --fy: entrance-only vertical offset; --fx: persistent spread */
@keyframes frame-enter {
  from { opacity: 0; filter: blur(12px); transform: translate(calc(var(--fx, 0px) + var(--fex, 0px)), var(--fy, 0px)); }
  to   { opacity: 1; filter: blur(0); transform: translate(var(--fx, 0px), 0); }
}

/* Plain entrance fallback */
.overlay-hero.enter-simple .hero-gradient {
  animation: base-enter 0.5s ease both;
}

/* Entrance: the subject settles first, then the world fills in, with the
   display title landing just a beat behind the background */
.overlay-hero.enter-depth .overlay-iso,
.overlay-hero.enter-depth .hero-wide-iso {
  animation: iso-enter 0.6s cubic-bezier(0.22, 1, 0.36, 1) var(--wiso-delay, 0s) both;
}

/* The gradient glow rises just behind the subject's arrival */
.overlay-hero.enter-depth .hero-gradient {
  animation: base-enter 0.55s ease 0.25s both;
}

.overlay-hero.enter-depth .hero-ambient,
.overlay-hero.enter-depth .hero-wide,
.overlay-hero.enter-depth .hero-scene {
  animation: base-enter 0.55s ease var(--bg-delay, 0.6s) both;
}

.overlay-hero.enter-depth .hero-type img {
  animation: type-enter 0.5s cubic-bezier(0.22, 1, 0.36, 1) var(--type-delay, 0.78s) both;
}

@keyframes type-enter {
  from { opacity: 0; transform: translateY(calc(var(--type-y, 0px) + 14px)); }
  to   { opacity: 1; transform: translateY(var(--type-y, 0px)); }
}

/* Per-project vertical nudge for the type layer */
.hero-type img {
  transform: translateY(var(--type-y, 0px));
}

@keyframes iso-enter {
  from { transform: translate(var(--ex, 0px), var(--ey, 72%)) scale(var(--iso-scale, 1)); opacity: 0; }
  30%  { opacity: 1; }
  to   { transform: translate(0, 0) scale(var(--iso-scale, 1)); opacity: 1; }
}

/* Per-project subject sizing inside the stage, anchored to the bottom */
.hero-stage .overlay-iso {
  transform: scale(var(--iso-scale, 1));
  transform-origin: 50% 100%;
}

/* Second subject state (Ballie's beam): sits exactly on the first and
   fades in when the type lands — the robot projecting its own name */
.hero-stage .hero-iso-b {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--kv-pos, 50% 100%);
  z-index: 1;
  transform: scale(var(--iso-scale, 1));
  transform-origin: 50% 100%;
  pointer-events: none;
}

.hero-iso-b[hidden] { display: none; }

.overlay-hero.enter-depth .hero-iso-b {
  animation: base-enter 0.5s cubic-bezier(0.22, 1, 0.36, 1) var(--type-delay, 0.78s) both;
}

/* The photo fades in at its final size — no scale, so it stays in
   register with the already-settled subject the whole time */
@keyframes base-enter {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* DOOH entrance (Beyond the Unknown): the street appears with NO display
   at all (the inpainted plate), sitting dark — a screen that is off casts
   no light. Then the display flickers to life, and every pulse throws
   light onto the street in sync, until the scene is fully lit. */
.overlay-hero.enter-flicker .hero-wide {
  animation:
    base-enter 0.45s ease both,
    street-ignite 1.5s linear 0.65s both;
}

.overlay-hero.enter-flicker .hero-wide-iso {
  animation: screen-flicker 1.5s linear 0.65s both;
}

@keyframes screen-flicker {
  0%   { opacity: 0; }
  6%   { opacity: 0.55; filter: brightness(1.6); }
  10%  { opacity: 0.05; }
  19%  { opacity: 0.75; filter: brightness(1.35); }
  24%  { opacity: 0.15; }
  33%  { opacity: 1; filter: brightness(1.5); }
  40%  { opacity: 0.35; }
  52%  { opacity: 1; filter: brightness(1.2); }
  60%  { opacity: 0.85; }
  74%  { opacity: 1; filter: brightness(1.08); }
  100% { opacity: 1; filter: brightness(1); }
}

/* The street's light level follows the screen, pulse for pulse */
@keyframes street-ignite {
  0%   { filter: brightness(0.42) saturate(0.75); }
  6%   { filter: brightness(0.72) saturate(0.85); }
  10%  { filter: brightness(0.46) saturate(0.75); }
  19%  { filter: brightness(0.78) saturate(0.88); }
  24%  { filter: brightness(0.5) saturate(0.78); }
  33%  { filter: brightness(0.88) saturate(0.95); }
  40%  { filter: brightness(0.55) saturate(0.8); }
  52%  { filter: brightness(0.94) saturate(0.98); }
  60%  { filter: brightness(0.82) saturate(0.92); }
  74%  { filter: brightness(1) saturate(1); }
  100% { filter: brightness(1) saturate(1); }
}



/* ---------- Tablet & below (≤ 899px) ---------- */
@media (max-width: 899px) {
  :root { --pad: 24px; }

  .clock { display: none; }

  .grid { grid-template-columns: repeat(2, 1fr); }
  .grid.uniform .card,
  .grid.duo .card,
  .grid.feature .card,
  .grid.feature .card.lead { grid-column: span 1; }

  .intro-role { white-space: normal; }
  html.landing .landing-intro { padding-right: clamp(16px, 4vw, 48px); }

  /* The graph keeps its desk-sized geometry; the finger scrolls it
     sideways. Wide enough that project names don't truncate, with room
     below for the lowest node */
  .ont-scroll {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    margin: 0 calc(var(--pad) * -1);
    padding: 0 var(--pad);
  }

  .ontology { min-width: 1400px; margin-bottom: 16px; }

  /* Detail page: the copy sits right under the hero, no forced first
     fold — the page is as tall as its content */
  .overlay-fold { min-height: 0; }
  .overlay-hero { --hero-h: min(56.25vw, 55svh); height: var(--hero-h); }
  .overlay-hero.pdp { max-height: min(56.25vw, 55svh); }
  .overlay-inner { padding: 28px var(--pad) 48px; }
  .overlay-title { font-size: 24px; line-height: 1.15; letter-spacing: -0.015em; }
  .overlay-summary { font-size: 15px; line-height: 1.45; }
  .overlay-fold:has(+ .overlay-case:not([hidden])) .overlay-inner { padding-bottom: 32px; }

  /* Scrolled glass carries a tint so white nav text holds over bright art */
  html.scrolled .topbar::before { background: rgba(0, 0, 0, 0.45); }

  /* The Close pill is glass, and steps aside while you read downward
     (it returns on the way back up and at the end of the page) */
  .overlay-close {
    --close-bg: rgba(28, 28, 30, 0.72);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    backdrop-filter: blur(20px) saturate(180%);
    transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.25s ease, background 0.2s ease;
  }

  .overlay.hide-close .overlay-close {
    transform: translateY(calc(100% + 40px));
    opacity: 0;
    pointer-events: none;
  }
}


/* ---------- Touch-first devices: finger-sized targets (≥ 44pt) ---------- */
@media (hover: none) and (pointer: coarse) {
  .prompt-chip { min-height: 44px; padding: 12px 18px; }
  .mode-btn { min-height: 44px; padding: 10px 16px; }
  .logo, .topbar-link, .intro-contact { padding: 12px 0; margin: -12px 0; }
  .overlay-close { padding: 14px 20px; }
  /* Graph nodes keep their look; the touch area grows invisibly */
  .ont-node::after { content: ""; position: absolute; inset: -8px 0; }
}

/* Compact touch screens in landscape have no room for the graph */
@media (max-height: 500px) and (pointer: coarse) {
  .mode-toggle { display: none; }
}

/* Phones scroll the document itself, so Safari's toolbar can collapse and
   a status-bar tap returns to the top. (Larger screens keep the inner
   scroller, which holds the fixed bar still through the bounce.) */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  body { height: auto; overflow: visible; overflow-x: clip; }
  .feed { height: auto; min-height: 100svh; overflow: visible; overflow-x: clip; }
  .overlay { overscroll-behavior: contain; }
}

/* ---------- Phone (≤ 700px) ----------
   The landing stacks (name at the top, the question reel at the bottom),
   the reel is finger-driven, the query pill wraps, the scan sheet is a
   3-across contact sheet, the detail page centers its text under a
   full-width hero, case-study pages open in a pan-to-read viewer, and
   the ontology graph steps aside (it needs a desk-sized canvas). */
@media (max-width: 700px) {
  :root { --pad: 16px; }

  .grid { grid-template-columns: 1fr; }

  /* Landing: name at the top, reel at the bottom — the thumb's reach.
     svh (not dvh) so the reel doesn't jump when Safari's bars collapse */
  html.landing .ask {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: stretch;
    gap: 24px;
    min-height: 100svh;
    padding-top: max(28px, env(safe-area-inset-top, 0px));
    padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
  }

  html.landing .landing-intro {
    justify-self: auto;
    padding-right: 0;
  }

  .landing-intro h1 { font-size: 17px; font-weight: 500; }

  html.landing .roulette {
    height: min(56svh, 460px);
    -webkit-mask-image: linear-gradient(to bottom, transparent, black 18%, black 82%, transparent);
    mask-image: linear-gradient(to bottom, transparent, black 18%, black 82%, transparent);
  }

  /* No arrow on a phone: the lit chip is the indicator */
  .roulette-ind { display: none; }

  .roulette .prompt-chip {
    white-space: nowrap;
    max-width: calc(100vw - var(--pad) * 2);
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .roulette .prompt-chip.hot { transform: scale(1.04); }

  /* The marquee rows fade at both edges instead of hard-cutting chips */
  .marquee {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent);
  }

  /* The query pill: centered, wrapping, sized to its text — JS sets an
     inline left/top for the desktop staging, which a phone overrides */
  .q-pill.stage {
    left: 50% !important;
    width: max-content;
    max-width: calc(100vw - var(--pad) * 2);
    white-space: normal;
    line-height: 1.5;
    text-align: center;
  }

  .ask-answer { font-size: 22px; }

  /* Tokens land fast enough that the caret never floats past a run of
     still-invisible text */
  .tok { animation-duration: 0.18s; }

  /* Scan sheet: 3 across, starting below the pill parked at 14vh (room
     for a pill that wraps to two lines once it is parsed) */
  .grid.scan {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
    padding: calc(14vh + 76px) var(--pad) 0;
    align-content: start;
  }

  /* The lit ring is drawn inside the card, on its own top layer (an inset
     outline would sit under the card's composited image layers), so two
     neighbouring matches light up side by side without their rings touching */
  .grid.scan .card.lit-wave { outline: none; }

  .grid.scan .card.lit-wave::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    border: 2px solid var(--wave-c, #4786e0);
    border-radius: inherit;
    pointer-events: none;
  }

  .grid.scan .scan-think {
    height: 96px;
    margin-bottom: 14px;
    font-size: 12px;
  }

  /* Past thoughts stay legible */
  .think-line { opacity: 0.6; color: rgba(255, 255, 255, 0.7); }

  .card-meta .meta-line.chip { font-size: 13px; }

  /* Detail page: full-width 16:9 canvas, nothing cropped; wide PDP art
     is cropped to 2:1 so the subject reads (every layer shares the box,
     so the crop stays in register). Wearable's three panels are the
     composition, so it keeps its own ratio. */
  .overlay-hero {
    --hero-h: 56.25vw;
    height: var(--hero-h);
  }

  .overlay-hero.pdp { aspect-ratio: 2 / 1; max-height: none; }

  /* The copy sits directly under the hero — one block, then the case
     study (if any) follows; short pages simply end */
  .overlay-fold { min-height: 0; }

  .overlay-inner { padding: 22px var(--pad) calc(88px + env(safe-area-inset-bottom, 0px)); }
  .overlay-fold:has(+ .overlay-case:not([hidden])) .overlay-inner { padding-bottom: 28px; }

  .overlay-title { font-size: 24px; line-height: 1.15; letter-spacing: -0.015em; }
  .overlay-summary { font-size: 15px; line-height: 1.45; margin-bottom: 20px; }
  .overlay-info { margin-bottom: 0; }
  .overlay-close { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
  .overlay-nav { padding-top: max(12px, env(safe-area-inset-top, 0px)); }

  /* Case study: full-bleed pages, room at the end for the Close pill */
  .overlay-case { padding: 0 0 calc(96px + env(safe-area-inset-bottom, 0px)); }
  .case-pages img { cursor: zoom-in; }

  /* The ontology graph needs a desk — the toggle steps aside */
  .mode-toggle { display: none; }

  .feed-foot { padding-top: 40px; }
}

/* ---------- Narrow phones (≤ 360px) ---------- */
@media (max-width: 360px) {
  /* Every question fits whole on the reel */
  .roulette .prompt-chip { font-size: 13px; padding: 12px 14px; }
  /* A two-line thought plus its count stays inside the console */
  .grid.scan .scan-think { height: 100px; margin-bottom: 8px; }
  .grid.scan { gap: 6px; padding-top: calc(14vh + 98px); padding-left: 20px; padding-right: 20px; }
}

/* ---------- Landscape phones (height ≤ 500px) ----------
   The pill parks at 14vh (≈48px); the console and a 5 × 3 sheet fit in
   the ~340px that remain */
@media (orientation: landscape) and (max-height: 500px) {
  .grid.scan {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 8px;
    padding: 100px max(24px, calc(50vw - 250px)) 0;
    align-content: start;
  }

  .grid.scan .card.lit-wave { outline: none; }

  .grid.scan .card.lit-wave::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    border: 2px solid var(--wave-c, #4786e0);
    border-radius: inherit;
    pointer-events: none;
  }

  .grid.scan .scan-think {
    height: 44px;
    margin-bottom: 8px;
    font-size: 12px;
  }

  .ask-answer { font-size: 22px; }
}

/* ---------- Phones: nothing floats over content ----------
   The detail's nav becomes a fixed, opaque bar that carries Close — the
   page scrolls beneath a solid strip instead of under a floating pill, so
   Close never covers a line of copy or a case page. The near-opaque stage
   layers drop their blur (it only cost frames on a phone GPU). */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .overlay-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 101;
    padding-right: calc(var(--pad) + 76px);
  }

  .overlay-fold { padding-top: calc(max(12px, env(safe-area-inset-top, 0px)) + 32px); }

  .overlay .overlay-close,
  .overlay.hide-close .overlay-close {
    top: calc(max(12px, env(safe-area-inset-top, 0px)) - 12px);
    bottom: auto;
    right: 4px;
    z-index: 102;
    min-height: 44px;
    padding: 0 12px;
    border-radius: 0;
    --close-bg: transparent;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    transform: none;
    opacity: 1;
    pointer-events: auto;
  }

  .grid.scan,
  body::after {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ---------- Touch ----------
   No sticky hovers after a tap; a press feedback instead */
@media (hover: none) {
  .prompt-chip:hover { background: rgba(255, 255, 255, 0.12); transform: none; }
  .prompt-chip.active:hover { background: var(--ink); color: var(--bg); }
  .roulette .prompt-chip.hot:hover { background: var(--ink); color: var(--bg); }
  .prompt-chip:active { background: rgba(255, 255, 255, 0.2); transform: scale(0.97); }
  .card:hover .card-layers { opacity: 0; }
  .card.decomp:hover .card-layers,
  .card:hover .card-layers.composed { opacity: 1; }
  .card:hover .card-layers .lay-bg,
  .card:hover .card-layers .lay-iso,
  .card:hover .card-layers .lay-type { transform: none; }
  .card:hover .meta-line.is-primary { color: var(--ink); }
  .card:active { transform: scale(0.985); transition: transform 0.12s ease; }
  .topbar-link:hover, .intro-contact:hover { opacity: 1; }
  .topbar-link:active, .intro-contact:active { opacity: 0.6; }
  .overlay-close:hover { background: var(--close-bg, var(--card)); }
  .overlay-close:active { background: var(--card-hover); }
}

/* ---------- Case-page viewer (phones) ----------
   A page tapped on a phone opens at reading size; drag to pan, pinch to
   zoom, tap anywhere to close */
.case-zoom {
  position: fixed;
  inset: 0;
  z-index: 130;
  background: #000;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x pan-y pinch-zoom;
  display: none;
}

.case-zoom.open { display: block; }

.case-zoom img {
  display: block;
  /* the page at its own pixel size — never upscaled past the source */
  width: auto;
  min-width: 100vw;
  max-width: none;
  height: auto;
}

.case-zoom-close {
  position: fixed;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: 12px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(28, 28, 30, 0.92);
  color: var(--ink);
  font-size: 18px;
  line-height: 44px;
  text-align: center;
}

.case-zoom-hint {
  position: fixed;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  padding: 10px 18px;
  border-radius: 999px;
  background: rgba(28, 28, 30, 0.9);
  color: var(--ink);
  font-size: 14px;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 0.5s ease;
}

.case-zoom.quiet .case-zoom-hint { opacity: 0; }

.case-zoom-close:focus:not(:focus-visible) { outline: none; }

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .grid.scan,
  .q-pill.stage,
  .q-key,
  .q-word.flipping-out,
  html.landing.landing-in .landing-intro,
  html.landing.landing-in .roulette,
  .grid.scan .card,
  .think-line,
  .think-line.live::before,
  .think-line.live .th-text,
  .th-n,
  .clock-alt,
  .clock-alt.repop,
  .roulette-ind.tick {
    animation: none !important;
  }

  .roulette .prompt-chip,
  .roulette-track,
  .grid.scan .card,
  .grid .card {
    transition: none !important;
  }
}
