/* ============================================================
   WHERE I CAN ADD VALUE — numbered grid
   ============================================================ */
/* no top padding: the hero is a full viewport with no bottom padding of its
   own, so this heading is the first thing past the fold and the section
   should meet the scroll rather than open on 100px of empty ground. Every
   other boundary keeps the standard 200px. */
.value { padding-top: 0; }

.value-list {
  list-style: none;
  counter-reset: value;
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  /* wider than the page gutter on purpose: these cells are loose paragraphs
     with no rule or edge between them, so the gap is the only thing keeping
     one column's rag from reading as the next one's first word */
  column-gap: clamp(var(--s-4), 4vw, var(--s-5));
  row-gap: clamp(var(--s-4), 6vh, var(--s-5));
  margin-top: var(--s-4);
}

/* Two tracks: the index, then the paragraph. The number is generated rather
   than authored, so the order comes off the markup — data/site.json carries
   no numbering, and adding or reordering an item renumbers the set for free.
   It is already an <ol>, so the sequence was in the DOM the whole time and
   this only makes it visible.

   Baseline alignment rather than start: the counter and the lead sit on one
   line, which is what stops the number reading as a bullet floating above
   the text. */
.value-item {
  grid-column: span 4;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--s-2);
  align-items: baseline;
}

/* decimal-leading-zero, so it is 01 rather than 1 — a fixed two-figure width
   keeps the paragraphs' left edges in one line all the way down. Tabular
   figures hold that alignment if the list ever runs past nine. */
.value-item::before {
  counter-increment: value;
  content: counter(value, decimal-leading-zero);
  color: var(--ink);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-variant-numeric: tabular-nums;
}

.value-item__text {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-dim);
  max-width: 34ch;
}
/* block, not a <br>: the lead takes its own line without putting a line
   break in the markup, so the pair still reads as one sentence to a screen
   reader and the body's leading space collapses at the start of its line */
.value-item__text strong {
  display: block;
  font-weight: 500;
  color: var(--ink);
}

@media (max-width: 1024px) {
  .value-item { grid-column: span 6; }
}
@media (max-width: 640px) {
  /* one real column, not twelve tracks with one item spanning them all. The
     column-gap above floors at --s-4 once 4vw drops below it, and eleven
     40px gaps come to 440px — more than the whole content width at this
     size. Tracks can collapse to zero but gaps cannot, so the grid pushed
     past the viewport and the page scrolled sideways.
     1 / -1 rather than span 12: against a single-column template, span 12
     would generate eleven implicit columns and bring the gaps right back. */
  .value-list { grid-template-columns: minmax(0, 1fr); }
  .value-item { grid-column: 1 / -1; }
  .value-item__text { max-width: none; }
}

/* ============================================================
   CAPABILITIES — subsection of "Where I can add value": an untitled
   spectrum list running the full width, flush left
   ============================================================ */
/* one gap, not two: this used to carry the same clamp as both margin and
   padding, which stacked into double the intended space — there is no rule
   or fill between the two blocks for the padding to sit inside of */
/* the same panel as the questions thread — one fill, one radius, one inset,
   all three from shared tokens. The value grid above it is bare paragraphs
   on the page, so the fill is what separates the capabilities line from them
   now that the section itself is no longer banded. */
.capabilities {
  margin-top: clamp(var(--s-4), 7vh, var(--s-5));
  border-radius: var(--radius);
  background: var(--panel);
  padding: var(--panel-pad);
}

.capabilities__grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--s-3);
  align-items: baseline;
}

/* The same trap .value-list fell into, one section up. Twelve tracks collapse
   to 0 on a narrow screen but their eleven 24px gaps cannot: the grid went on
   demanding 264px of gap inside a 222px panel, so the list — spanning 1 / -1
   — was handed a column wider than its own container, its nowrap items ran to
   348px, and the whole page scrolled sideways with them.

   One column, so there are no gaps left to pay for. This block sits AFTER the
   base rules on purpose: the mobile padding override used to live in the
   max-width: 640px block above them, where equal specificity and earlier
   source order meant it never applied at all and the panel kept its full
   60px inset. */
@media (max-width: 860px) {
  .capabilities__grid { grid-template-columns: minmax(0, 1fr); }
  /* half the desktop inset. The list is set at the h1 rung, and 120px of
     horizontal padding is width the type needs far more than the frame. */
  .capabilities { padding: calc(var(--panel-pad) / 2); }
}

/* the section lost its heading, so the list labels itself. Generated content
   rather than an <li> on purpose: a real first child would shift every
   :nth-child spectrum colour below by one, and pick up a comma */
.capabilities__list::before {
  content: "Capabilities: ";
  color: var(--ink);
  /* Deliberately NOT nowrap. "Capabilities:" is one word with no internal
     break opportunity, so nowrap bought nothing here — but it also swallowed
     the trailing space in the content string, and a space inside a nowrap box
     is not a place a line may break. That glued the label to whichever item
     came first: on a phone the two ran 347px across a 282px line and took the
     page sideways. Normal, and the label simply keeps its own line when the
     first item won't fit beside it. */
  white-space: normal;
}

.capabilities__list {
  grid-column: 1 / -1;
  list-style: none;
  /* same step as .section__title, so the spectrum reads as a heading
     rather than a subhead */
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  letter-spacing: -0.02em;
  font-weight: var(--fs-heading-weight);
}

/* nowrap keeps a two-word label and its comma on one line together —
   items only ever break between each other, never inside one */
.capabilities__item {
  display: inline;
  white-space: nowrap;
}
.capabilities__item:not(:last-child)::after { content: ","; }

/* full spectrum, pink through violet; cycles every 8 if the list grows */
.capabilities__item:nth-child(8n + 1) { color: #ec4899; }
.capabilities__item:nth-child(8n + 2) { color: #f0432b; }
.capabilities__item:nth-child(8n + 3) { color: #f97316; }
.capabilities__item:nth-child(8n + 4) { color: #f5c518; }
.capabilities__item:nth-child(8n + 5) { color: #5ec84f; }
.capabilities__item:nth-child(8n + 6) { color: #2dd4bf; }
.capabilities__item:nth-child(8n + 7) { color: #3b9ef5; }
.capabilities__item:nth-child(8n + 8) { color: #6366f1; }

/* no narrow-screen step-down: the list tracks --fs-h1 at every width, so it
   stays the same size as the .section__title headings around it. The longest
   item ("Product design,") plus its icon still fits a 320px viewport at the
   28px mobile step, with the items wrapping between each other as needed */

/* ---------- 3D layered scenes ---------- */
.p3d {
  position: relative;
  width: 100%;
  height: 100%;
  perspective: 900px;
}
.p3d__scene {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.45s var(--ease-out), transform 0.45s var(--ease-out);
}
.p3d__scene.is-active { opacity: 1; transform: none; }

.p3d__world {
  position: relative;
  width: 56%;
  aspect-ratio: 4 / 5;
  transform-style: preserve-3d;
  will-change: transform;
}
.p3d__layer {
  position: absolute;
  border-radius: var(--radius-sm);
  transition: transform 0.5s var(--ease-out);
}

/* base plate — the "photo" card */
.p3d__plate {
  inset: 0;
  transform: translateZ(0);
  box-shadow: 0 48px 90px rgba(0, 0, 0, 0.55);
}

/* mid panel — the "screen/content" layer */
.p3d__panel {
  left: 9%;
  right: 9%;
  top: 20%;
  height: 46%;
  background: #f2f2f5;
  transform: translateZ(64px);
  padding: 9% 8%;
  display: flex;
  flex-direction: column;
  gap: 9%;
  box-shadow: 0 24px 50px rgba(0, 0, 0, 0.4);
}
.p3d__panel-bar {
  width: 38%;
  height: 9%;
  border-radius: 99px;
  background: #1c1c22;
}
.p3d__panel-line {
  height: 5.5%;
  border-radius: 99px;
  background: #c9c9d1;
}
.p3d__panel-media {
  flex: 1;
  border-radius: 8px;
}

/* floating chips — highest layers, overhanging the plate */
.p3d__chip {
  top: -5%;
  right: -9%;
  transform: translateZ(130px);
  padding: 8px 16px;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  font-weight: 400;
  color: #fff;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
.p3d__pill {
  bottom: -4%;
  left: -8%;
  transform: translateZ(170px);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  font-weight: 400;
  color: var(--ink);
  background: rgba(13, 13, 15, 0.9);
  border: 1px solid var(--hairline);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}

/* hover: layers lift apart slightly */
@media (hover: hover) {
  .p3d:hover .p3d__panel { transform: translateZ(84px); }
}
@media (hover: hover) {
  .p3d:hover .p3d__chip { transform: translateZ(158px); }
}
@media (hover: hover) {
  .p3d:hover .p3d__pill { transform: translateZ(205px); }
}

/* ============================================================
   AI WORKFLOWS SCENE — a product-reveal film: light in a dark
   studio reveals a terminal, then a device building its own UI.
   One canvas; the parent 3D rig supplies hover dimensionality.
   ============================================================ */
.ai-scene {
  width: 100%;
  aspect-ratio: 1;
}
.ai-canvas {
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  pointer-events: none;
}
.ai-canvas--film { transform: translateZ(50px); }
@media (hover: hover) {
  .p3d:hover .ai-canvas--film { transform: translateZ(80px); }
}


/* ============================================================
   CASE STUDIES
   ============================================================ */
/* A snapping carousel at every width: one big card fills most of the row on
   desktop, with the next one peeking in from the edge to signal there's
   more to scroll to. No hover lift or drop shadow here — overflow-x clips
   them, and a lift inside a scroller reads as a glitch mid-swipe. */
.cases__row {
  /* the row spans the viewport, so the trailing card runs off the screen edge
     instead of being cut at the container's margin. This padding reproduces
     .container's offset, keeping the first card on the page grid — and it
     collapses to the plain page margin once the viewport is under --container */
  --edge: max(var(--page-margin), (100% - var(--container)) / 2);
  padding-inline: var(--edge);
  /* without this, snapping aligns cards to the scrollport edge and scrolls
     the leading offset out of view */
  scroll-padding-inline: var(--edge);
  display: flex;
  /* not stretch: cards get their height from aspect-ratio, and stretching
     them against it is one more thing that can wobble as the row resizes */
  align-items: flex-start;
  gap: var(--gutter);
  margin-top: var(--s-5);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.cases__row::-webkit-scrollbar { display: none; }

/* Setting overflow-x makes overflow-y compute to auto, so this row clips and
   scrolls vertically as well. The global reveal starts elements 12px low,
   which inside here means the bottom of every card is cut while it animates
   and the transient overflow flickers a scrollbar. Fade them in instead —
   the horizontal entrance is what this row is about anyway. */
.cases__row [data-reveal] { transform: none; }

.case-card {
  /* ten of the page's twelve columns — the same --col/--gutter grid math
     .hero__content uses, rather than an arbitrary fraction of the row.
     One card now fills most of the row, with just a sliver of the next
     peeking in — bigger and steadier than the old 1.5-cards fraction. */
  --span: 10;
  flex: 0 0 calc(var(--span) * var(--col) + (var(--span) - 1) * var(--gutter));
  scroll-snap-align: start;
  position: relative;
  /* the card IS the image now — the copy sits on top of it */
  aspect-ratio: 4 / 3;
  text-align: left;
  border-radius: var(--radius);
  background: var(--bg-card);
  /* transparent rather than none, so the card doesn't shift on hover */
  border: 1px solid transparent;
  overflow: hidden;
  transition: border-color 0.4s ease;
}
@media (hover: hover) {
  .case-card:hover { border-color: rgba(255, 255, 255, 0.18); }
}

/* 40px was drawn for a card that fills a desktop row. At phone width the same
   arc eats a visible bite out of a much smaller cover image, so it tightens. */
@media (max-width: 860px) {
  .case-card { border-radius: 20px; }
}
/* no url yet, so this one is an <article> rather than a link. It sits still
   on hover as well: the border lift and the cover zoom both read as "this
   opens", and there is nothing here to open. */
@media (hover: hover) {
  .case-card--static:hover { border-color: transparent; }
}

.case-card__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* dark placeholder until real cover images exist */
  background: linear-gradient(145deg, #17171a 0%, #0e0e10 100%);
}
.case-card__media img,
.case-card__media video {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.8s var(--ease-out), opacity 0.5s;
}
@media (hover: hover) {
  .case-card:hover .case-card__media img,
  .case-card:hover .case-card__media video { transform: scale(1.05); }
}
@media (hover: hover) {
  .case-card--static:hover .case-card__media img,
  .case-card--static:hover .case-card__media video { transform: none; }
}

/* copy overlaid on the cover, pinned to the bottom of every card. The scrim
   is part of the body rather than the media so it always sits directly
   under the text, however long the teaser runs */
.case-card__body {
  position: absolute;
  inset: auto 0 0 0;
  /* 30px sides and foot. The top stays --s-5: that is the gradient's run-up,
     not an inset, and the copy needs the dark to have arrived before it. */
  padding: var(--s-5) 30px 30px;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.8),
    rgba(0, 0, 0, 0.55) 45%,
    transparent
  );
}
.case-card__title {
  font-size: var(--fs-h4);
  line-height: var(--lh-h4);
  letter-spacing: -0.01em;
  font-weight: 500;
  color: #fff;
  margin-bottom: 2px;
}
.case-card__teaser {
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  color: rgba(255, 255, 255, 0.78);
}

/* ---- carousel arrows, bottom-right of the row ---- */
.cases__nav {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-1);
  margin-top: var(--s-3);
}
.cases__nav[hidden] { display: none; } /* beats the flex above */

.cases__arrow {
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
  cursor: pointer;
  transition: background 0.25s ease, opacity 0.25s ease;
}
@media (hover: hover) {
  .cases__arrow:hover { background: rgba(255, 255, 255, 0.16); }
}
.cases__arrow:active { transform: scale(0.94); }
.cases__arrow:disabled { opacity: 0.3; cursor: default; }
@media (hover: hover) {
  .cases__arrow:disabled:hover { background: rgba(255, 255, 255, 0.08); }
}
.cases__arrow svg { width: 20px; height: 20px; }

/* Same 44px circle and 20px glyph as .cases__arrow below the row, so the two
   controls on this section read as one size. The fill is the sticky header's
   exact treatment — a dark translucent slab over a saturated blur — and it
   carries no stroke, so the blur alone separates it from the cover art. */
.case-card__lock {
  position: absolute;
  top: 12px; right: 12px;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(5, 5, 5, 0.72);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  color: var(--ink);
}
/* overrides the width/height attributes on the inline svg */
.case-card__lock svg { width: 20px; height: 20px; }

/* mobile: one card at a time, with the next peeking. The bleed and the
   leading offset already come from --edge, which collapses to the page
   margin at these widths */
@media (max-width: 860px) {
  /* the card now runs nearly the full content column — 28px short of it —
     instead of the old 78%, so the active card reads as the screen. What's
     left over plus the page margin is the peek: with the gap halved to 12px,
     the next card always shows edge + 28 - 12 = a 40px sliver, the same
     width at 320px as at 860px because it's a fixed subtraction, not a
     percentage. Scoped to .cases so the jimeo phone row keeps its own gap. */
  .cases .cases__row { gap: 12px; }
  .case-card { flex: 0 0 calc(100% - 28px); }
  /* the 30px inset is a desktop measure; at phone width the card is barely
     wider than the copy, so the foot goes back to the page's own rhythm */
  .case-card__body { padding: var(--s-5) var(--s-3) var(--s-3); }
  /* swiping is the affordance here, so the arrows would be redundant */
  .cases__nav { display: none; }
}

/* ============================================================
   FAQs — message thread
   Thread panel runs the full 12-column width. Questions sit right,
   replies left.
   ============================================================ */
/* no padding override here: this section takes the standard .section value
   like every other one. It used to pull up to about half, back when the
   standard padding was sized to hold a hairline in the middle of it — with
   the dividers gone the gap is the separator, so an odd one out reads as a
   mistake rather than as tightening. */

.faq__grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--s-4);
  align-items: start;
}
/* title centred over the thread rather than sitting off to its left */
.faq__title {
  grid-column: 1 / -1;
  text-align: center;
  margin-bottom: 0;
}

/* the thread sits in a panel, like a conversation window, running the full
   12-column width. The title above stays centred on the same axis either
   way. */
.faq__thread {
  grid-column: 1 / -1;
  border-radius: var(--radius);
  background: var(--panel);
  padding: var(--panel-pad);
}

/* Flex rather than plain flow so the thread can be ordered by when a
   question was asked instead of by where it sits in the markup. Everything
   stays in the DOM in authored order — only `order` moves — so the answers
   remain indexable and findable by find-in-page. */
.faq-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  /* gap, not `.faq-item + .faq-item`: an adjacent-sibling margin follows DOM
     order, which stops matching what is on screen the moment anything moves.
     One value everywhere, so a reply sits the same distance from the question
     above it as from the question below it. */
  gap: var(--s-2);
}

/* the pool of questions not yet asked, held at the foot of the thread. A
   single shared order value, so among themselves they keep authored order. */
.faq-item { order: 999; }
/* the opener is always the first thing said */
.faq-item--intro { order: 0; }
/* asked questions get an explicit order in tap sequence, assigned in ask.js —
   between the opener and the remaining pool */

.faq-row { display: flex; }
.faq-row--sent { justify-content: flex-end; }
.faq-row--received { justify-content: flex-start; }

/* avatar beside every bubble from Evan. Bottom-aligned rather than centred,
   so it sits on the same baseline as the bubble no matter how many lines
   that bubble runs to — which is how a chat thread reads, the face at the
   foot of the message rather than floating beside its middle. */
.faq-row--received {
  align-items: flex-end;
  gap: var(--s-1);
}

/* half the pill's height, derived rather than typed: the pill is its own
   vertical padding plus one line of body copy, so the circle follows any
   change to the ramp or the padding without being re-measured */
.faq-avatar {
  --pill-h: calc(24px + var(--fs-body) * var(--lh-body));
  position: relative;
  flex: none;
  width: calc(var(--pill-h) / 2);
  height: calc(var(--pill-h) / 2);
  border-radius: 50%;
  overflow: hidden;
}
.faq-avatar img { width: 100%; height: 100%; object-fit: cover; }

/* the face marks where the thread currently ends, so only the last message
   from Evan shows one. visibility, not display: the hidden avatars still
   occupy their slot, which is what keeps every received bubble indented by
   the same gutter and their left edges in one column. */
.faq-avatar { visibility: hidden; }
.faq-row--tail .faq-avatar { visibility: visible; }

/* the ring rides on a pseudo-element, not on the image: an inset box-shadow
   paints behind replaced content, so on the <img> itself it would be hidden */
.faq-avatar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.2);
}

/* 28px is half the height of a one-line bubble (13px padding x2 + a
   21px line at 1.4), so wrapped answers curve exactly like a pill
   instead of stretching into a stadium */
.faq-bubble {
  max-width: 74%;
  padding: 12px 24px;
  border-radius: var(--radius-bubble);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  text-align: left;
}

/* sent: outlined until tapped, then the iMessage blue. Fully round while it
   is still a prompt — it is a control at that point, not a message. */
.faq-bubble--sent {
  border: 1px solid rgba(255, 255, 255, 0.22);
  color: var(--ink-dim);
  cursor: pointer;
  transition: color 0.25s ease, border-color 0.25s ease,
              background 0.25s ease, transform 0.2s var(--ease-out);
}
@media (hover: hover) {
  .faq-bubble--sent:hover {
    color: var(--ink);
    border-color: rgba(255, 255, 255, 0.45);
  }
}
.faq-bubble--sent:active { transform: scale(0.97); }
/* once asked it is a message, so it takes the tail the replies have —
   mirrored to the sending side, since this bubble hangs off the right edge */
.faq-bubble--sent.is-active {
  background: #0a84ff;
  border-color: #0a84ff;
  color: #fff;
  border-bottom-right-radius: 8px;
  /* and the cursor light goes out with it. A sent question is inert — ask.js
     returns early on a re-tap — so an edge that still answered the pointer
     would be promising something that can no longer happen. Same reasoning
     and same mechanism as .btn--glow:disabled: js/glow.js goes on writing
     --glow-o to the element's style attribute regardless, and !important is
     what outranks that inline value. */
  --glow-o: 0 !important;
}

/* received: the reply */
/* Full 74% here, same as a sent bubble — the narrowing is a mobile-only move,
   see the max-width: 860px block. Tried at this width and it was wrong: the
   thread runs over 1000px, so pulling replies in two columns left the two
   sides no longer reaching each other. They stopped overlapping in the middle
   and the thread read as two parallel columns rather than one conversation.
   The overlap is what makes it a conversation. */
.faq-bubble--received {
  background: #1c1c1e;
  border-bottom-left-radius: 8px;
  color: var(--ink);
  min-height: 46px;
}

/* the answer text and the typing dots share one bubble and swap by class,
   so the answer stays in the DOM the whole time — findable by find-in-page
   and by crawlers even while the reply is collapsed. Only the 1.1s typing
   beat takes it out, and the bubble sizes to the dots first, then the text */
/* links inside an answer. The global reset strips colour and underline from
   every <a>, so without this a link in a bubble is invisible as one. Colour
   alone carries it — the accent against the bubble's own text is enough.
   Scoped to received bubbles: the sent ones are buttons, not prose. */
.faq-bubble--received a {
  color: var(--accent);
}

.faq-answer { display: block; }
.faq-bubble--received.is-typing .faq-answer { display: none; }

/* accordion: 0fr → 1fr so the reply grows the thread downward */
.faq-reply {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s var(--ease-out);
}
.faq-reply.is-open { grid-template-rows: 1fr; }
.faq-reply__inner { overflow: hidden; }
.faq-reply .faq-row { padding-top: var(--s-2); }

/* bubbles pop in the way a message lands */
.faq-reply .faq-bubble--received {
  opacity: 0;
  transform: translateY(8px) scale(0.94);
  transform-origin: left bottom;
  transition: opacity 0.3s ease, transform 0.4s var(--ease-spring);
}
.faq-reply.is-open .faq-bubble--received {
  opacity: 1;
  transform: none;
}

/* typing indicator — hidden until its bubble is mid-type */
.faq-typing { display: none; align-items: center; gap: 5px; height: 20px; }
.faq-bubble--received.is-typing .faq-typing { display: inline-flex; }
.faq-typing i {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--ink-faint);
  animation: faq-dot 1.2s ease-in-out infinite;
}
.faq-typing i:nth-child(2) { animation-delay: 0.15s; }
.faq-typing i:nth-child(3) { animation-delay: 0.3s; }
@keyframes faq-dot {
  0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}

/* compose bar, anchoring the bottom of the thread */
.faq-compose {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  /* the same --s-2 .faq-list puts between messages, not a step more: the
     compose row is the next thing in the thread, so it sits at the thread's
     own interval rather than being set apart from it */
  margin-top: var(--s-2);
}
/* no blanket hover fade here: the row signals hover through its own parts —
   the border brightens, the text swaps to the address, the send button goes
   blue — and dimming the whole thing muted the button at the moment it lit */
/* the field holds the placeholder and the send button, so the button
   sits inside the input rather than beside it */
/* 12px around the 38px button. Deliberately taller than the one-line bubble
   it used to match: the field is the one thing here you act on, so it gets
   more room than the messages above it. */
.faq-compose__field {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  /* Exactly a one-line bubble: its 12px padding twice, its 1px border twice,
     and one line of --fs-body. Same expression the bubbles resolve to, so the
     compose row sits on the thread's own rhythm instead of standing 13px
     taller than every message above it.

     The height is stated rather than left to the padding because the 38px
     send button, not the text, was setting it. With no vertical padding the
     button centres itself in the remaining 49px and the row stops growing
     around it. */
  min-height: calc(26px + var(--fs-body) * var(--lh-body));
  padding: 0 6px 0 22px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--radius-pill);
  transition: border-color 0.25s ease;
}
@media (hover: hover) {
  .faq-compose:hover .faq-compose__field { border-color: rgba(255, 255, 255, 0.45); }
}

.faq-compose__placeholder {
  flex: 1;
  min-width: 0;
  color: #5a5a5e; /* a step darker than --ink-faint, like a real placeholder */
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  transition: color 0.25s ease;
}
/* hover swaps the prompt for the address it actually opens */
.faq-compose__hover { display: none; }
.faq-compose:focus-visible .faq-compose__idle { display: none; }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__idle { display: none; }
}
.faq-compose:focus-visible .faq-compose__hover { display: inline; }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__hover { display: inline; }
}
.faq-compose:focus-visible .faq-compose__placeholder { color: var(--ink); }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__placeholder { color: var(--ink); }
}
/* Touch never gets the hover swap, so it takes its own string: short enough
   not to crowd the row at phone width, and a prompt rather than the raw email
   address, which read as a field that had already been filled in. */
.faq-compose__touch { display: none; }

@media (hover: none) {
  .faq-compose__idle,
  .faq-compose__hover { display: none; }
  .faq-compose__touch { display: inline; }
  .faq-compose__placeholder { color: var(--ink-dim); }
}
/* bare icon, no chip: it labels the field rather than acting as a control,
   so it carries the placeholder's color and shifts with it on hover */
.faq-compose__mail {
  flex: none;
  display: grid;
  place-items: center;
  color: #5a5a5e;
  transition: color 0.25s ease;
}
/* stroked, not filled — the colour rides `color` through stroke="currentColor"
   in the markup, so the hover rule below still drives it */
.faq-compose__mail svg { width: 20px; height: 20px; }
.faq-compose:focus-visible .faq-compose__mail { color: var(--ink); }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__mail { color: var(--ink); }
}
@media (hover: none) {
  .faq-compose__mail { color: var(--ink-dim); }
}

/* reads inert until the row is hovered, then lights up the way an iMessage
   send button does once there's something to send. Purely visual — the whole
   row is one mailto link, so the button was never separately disabled. */
.faq-compose__send {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border-radius: 50%;
  background: #2c2c2e;
  transition: background-color 0.25s ease;
}
.faq-compose__send svg { width: 19px; height: 19px; stroke: #8e8e93; transition: stroke 0.25s ease; }
.faq-compose:focus-visible .faq-compose__send { background: #0a84ff; }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__send { background: #0a84ff; }
}
.faq-compose:focus-visible .faq-compose__send svg { stroke: #fff; }

@media (hover: hover) {
  .faq-compose:hover .faq-compose__send svg { stroke: #fff; }
}
/* no hover to light it up on touch, so it ships in the active state */
@media (hover: none) {
  .faq-compose__send { background: #0a84ff; }
  .faq-compose__send svg { stroke: #fff; }
}

@media (max-width: 860px) {
  /* the panel goes away entirely at this width. Even a fraction of the
     --panel-pad inset was width the bubbles needed more than the frame did —
     a conversation window reads as a window on a desktop, but on a phone the
     whole screen is the window. Dropping the background with it means the
     bubbles run edge to edge inside the page margins. */
  .faq__thread {
    background: none;
    border-radius: 0;
    padding: 0;
  }

  /* ...and the fill moves up to the section, which runs the full width of the
     screen. Dropping the thread's panel left the questions floating between
     two unbounded sections with nothing marking where they start and stop.
     Putting it here frames the block and takes the heading in with it, rather
     than boxing the bubbles a second time.

     No radius and no horizontal padding: it bleeds to both screen edges, so
     there are no corners to round, and .container inside still holds the
     content on the page margins. */
  .faq {
    background: var(--panel);
    /* Every other section boundary gets 50px of dark from each neighbour, so
       100px between them. This one lost half of that the moment the section
       took a fill: its own 50px padding is inside the gray now and no longer
       reads as a gap. The margin puts the missing half back, off the same
       clamp .section uses so the two never drift apart. */
    margin-block: clamp(32px, 7vh, 50px);
  }
  /* The messages hold --fs-body at every width. This used to step down a rung
     to --fs-small, because at 85% of a phone's width less 24px of side padding
     body copy only got ~25 characters to a line. The measure was the real
     problem there, not the size — so it comes back out of the margins instead
     of out of the type: 92% of the row rather than 85%, on 18px of side
     padding rather than 24. That returns ~32 characters at the larger size,
     more than the 25 the smaller one was getting.

     No border-radius override any more either. The pill curve is half a
     one-line bubble, and with the ramp back to --fs-body that height is the
     same as the desktop one — so --radius-bubble from the base rule is
     already the right number, and there is nothing left here to keep in sync. */
  .faq-bubble { max-width: 92%; padding: 12px 18px; }
  /* Replies stop two of the twelve columns short of the right margin, so the
     sent side runs to one edge and the received side to the other and neither
     fills the line — the iMessage read, where shape tells you who is talking
     before colour does.
     Mobile only. At this width the measure is narrow enough that the two sides
     still overlap in the middle; on a desktop-width thread the same move pulls
     them apart into two columns. */
  .faq-bubble--received { max-width: calc(92% - 2 / 12 * 100%); }
}

/* ============================================================
   GALLERY — masonry + infinite scroll
   ============================================================ */
/* the heading centres over the full-bleed masonry. Scoped to .gallery so the
   other sections keep their left-aligned titles; the note also needs its
   52ch measure re-centred, since that cap is what holds its width */
.gallery .section__title,
.gallery .section__note {
  text-align: center;
}
.gallery .section__note { margin-inline: auto; }

/* Double the standard section gap below the archive. The game strip is 210px
   tall and sits on the footer's top edge, so at the normal gap it would cover
   the last row of images; this gives it empty ground to land on. It also just
   lets the grid finish before the footer starts. */
.gallery {
  padding-bottom: clamp(calc(var(--s-6) * 2), 28vh, calc(var(--s-7) * 2));
}

.gallery__masonry {
  display: flex;
  gap: var(--gutter);
  align-items: flex-start;
  margin-top: var(--s-5);
}
.masonry-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--gutter);
  min-width: 0;
}

.tile {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-raised);
  border: 1px solid transparent; /* stroke appears on hover only */
  /* without this the background paints under that transparent border too,
     leaving a 1px --bg-raised ring around every tile at rest */
  background-clip: padding-box;
  cursor: zoom-in;
  opacity: 0;
  transform: translateY(24px) scale(0.98);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out),
              border-color 0.3s ease;
}
.tile:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.tile.is-visible { opacity: 1; transform: none; }
@media (hover: hover) {
  .tile:hover { border-color: rgba(255, 255, 255, 0.2); }
}

.tile img, .tile svg, .tile canvas, .tile video {
  width: 100%;
  height: auto;
}
.tile img, .tile video { transition: transform 0.7s var(--ease-out); }
@media (hover: hover) {
  .tile:hover img,
  .tile:hover video { transform: scale(1.03); }
}
/* the clip is letterboxed on black, which would otherwise sit a shade off
   the tile's own surface */
.tile video { display: block; background: #000; }

/* SVG frame-sequence tiles — artwork spans the center third of the
   width; vertical padding is half the horizontal visual weight */
.tile--sequence {
  padding: 22.25% 33.333%;
  background: var(--bg);
}
.tile--sequence img {
  display: block;
  width: 100%;
  height: auto;
  background: #fff;
}

/* ============================================================
   WIDGET TILES — live interactive pieces
   ============================================================ */
.tile--widget { cursor: default; }
@media (hover: hover) {
  .tile--widget:hover { border-color: transparent; }
} /* no lightbox → no hover affordance */

/* glow button scene (colors sampled from the source recording) */
.glow-scene {
  container-type: inline-size;
  background: #010011;
  display: grid;
  place-items: center;
  aspect-ratio: 714 / 374;
}
.glow-btn {
  --gx: 50%;
  --gy: 0%;
  --glow-o: 0;
  position: relative;
  width: 46cqw;
  aspect-ratio: 330 / 115;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(124, 104, 238, 0.28);
  overflow: hidden;
  cursor: pointer;
  background: transparent; /* ghost button — the glow supplies all fill */
  transition: transform 0.25s var(--ease-spring);
}
.glow-btn:active { transform: scale(0.97); }

/* cursor-following interior glow */
.glow-btn__glow {
  position: absolute;
  inset: 0;
  /* --glow-rgb shifts purple → blue with cursor x (set in widgets.js) */
  background: radial-gradient(
    36cqw circle at var(--gx) var(--gy),
    rgba(var(--glow-rgb, 150, 122, 255), 0.34),
    rgba(var(--glow-rgb, 150, 122, 255), 0.1) 45%,
    transparent 75%
  );
  opacity: var(--glow-o);
}

/* border rim that brightens only near the cursor */
.glow-btn__rim {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(var(--glow-rgb, 196, 176, 255), 0.55);
  -webkit-mask-image: radial-gradient(
    22cqw circle at var(--gx) var(--gy),
    #000 15%, transparent 65%
  );
  mask-image: radial-gradient(
    22cqw circle at var(--gx) var(--gy),
    #000 15%, transparent 65%
  );
  opacity: calc(var(--glow-o) * 0.8);
}

.glow-btn__label {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  color: #f4f2ff;
  font-size: 4.8cqw;
  font-weight: 400;
  letter-spacing: 0.01em;
}
.glow-btn .glow-btn__icon {
  /* em-based so it tracks the label's type size; extra specificity
     needed to beat the generic `.tile svg { width: 100% }` rule */
  width: 0.95em;
  height: 0.95em;
  flex: none;
}

.gallery__sentinel { height: 1px; }

@media (max-width: 560px) {
  .gallery__masonry { gap: var(--s-1); }
  .masonry-col { gap: var(--s-1); }
}

/* ============================================================
   LIGHTBOX — tile expands in place (FLIP) to a centered view
   ============================================================ */
.lightbox-backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.78);
  backdrop-filter: blur(20px) saturate(120%);
  -webkit-backdrop-filter: blur(20px) saturate(120%);
  opacity: 0;
  transition: opacity 0.5s ease;
  cursor: zoom-out;
}
.lightbox-backdrop.is-open { opacity: 1; }

.lightbox-media {
  position: fixed;
  z-index: 201;
  overflow: hidden;
  border-radius: var(--radius-sm);
  transform-origin: top left;
  transition: transform 0.6s var(--ease-out), border-radius 0.6s var(--ease-out);
  will-change: transform;
  cursor: zoom-out;
  box-shadow: 0 48px 140px rgba(0, 0, 0, 0.7);
}
.lightbox-media.is-open { border-radius: var(--radius); }
.lightbox-media img,
.lightbox-media svg,
.lightbox-media canvas {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* sequence tiles expand as the whole tile: same bg + inset proportions.
   NB: % padding on this fixed-position box resolves against the
   viewport, so size the child instead */
.lightbox-media--sequence {
  background: var(--bg);
  display: grid;
  place-items: center;
}
.lightbox-media--sequence img {
  width: 33.333%;
  height: auto;
  background: #fff;
}

/* .lightbox-caption is gone with the element js/gallery.js used to build —
   the expanded view carries no text now. */

.lightbox-close {
  position: fixed;
  z-index: 202;
  top: 20px;
  right: 20px;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  font-size: 0; /* hide the &times; glyph — cross is drawn below */
  color: var(--ink);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--hairline);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  opacity: 0;
  transition: opacity 0.4s ease 0.3s, background 0.25s,
              transform 0.35s var(--ease-spring);
}
.lightbox-close::before,
.lightbox-close::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 16px; height: 1.5px;
  border-radius: 1px;
  background: currentColor;
  translate: -50% -50%;
}
.lightbox-close::before { rotate: 45deg; }
.lightbox-close::after { rotate: -45deg; }
.lightbox-close.is-open { opacity: 1; }
@media (hover: hover) {
  .lightbox-close:hover { background: rgba(255, 255, 255, 0.16); transform: rotate(90deg); }
}

.gallery__loader {
  display: flex;
  justify-content: center;
  padding: var(--s-4) 0;
}
.gallery__loader span {
  width: 6px; height: 6px;
  margin: 0 4px;
  border-radius: 50%;
  background: var(--ink-faint);
  animation: loader-pulse 1.2s ease-in-out infinite;
}
.gallery__loader span:nth-child(2) { animation-delay: 0.15s; }
.gallery__loader span:nth-child(3) { animation-delay: 0.3s; }
@keyframes loader-pulse {
  0%, 100% { opacity: 0.3; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1.2); }
}
