/* drift's surface. Every selector below is scoped to .drift-surface so this
 * cannot collide with public/styles.css, public/press.css or the board's sheet
 * — test/drift-client-guards.test.ts enforces that.
 *
 * The palette binds the shared --t0/--t1/--t2/--pin token contract to the
 * NIGHT-WALK values, matching public/index.html rather than press.css. Two
 * reasons, both in the spec: SPEC.md's tier guardrail is this palette ("pale
 * slate -> lilac -> warm ember; pinned = gold"), and press.css is explicitly
 * "two tones, not a ramp", so it structurally cannot carry the card's
 * strangeness cue.
 *
 * The tokens live on .drift-surface, NOT on :root. A global :root would leak
 * this binding to any page that ever loads this sheet, which is the same
 * collision the scoping rule exists to prevent — the earlier version claimed
 * full scoping while defining every colour globally. Critic cycle 1. */
.drift-surface {
  --ink: #0d0c14;
  --field: #151327;
  --deep: #100f1e;
  --t0: #cfd4e8;
  --t1: #b8a6e8;
  --t2: #e8a68f;
  --pin: #f0d98c;
  --label: #9a97b0;
  --faint: #565378;
  --hair: rgb(232 233 240 / 0.14);
  /* The stack's hierarchy, measured rather than eyeballed. The card, the ghosts
     and the ground were 1.03-1.07:1 apart — mathematically indistinguishable,
     which is why the top of the stack did not read as the thing you touch.
     On a ground this dark a FILL cannot reach the 3:1 WCAG 1.4.11 asks of a
     component boundary without going grey and leaving the night palette, so the
     definition is carried by the EDGE. --card-edge measures 3.12:1 against the
     ground; --ghost-edge deliberately does not, so the layers cannot be
     confused for each other. */
  --card: #242240;
  --card-edge: #5e589f;
  --ghost-edge: rgb(232 233 240 / 0.10);
  --serif: 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --sans: 'Space Grotesk', ui-sans-serif, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* border-box throughout. Without it the card's width: 100% plus its padding
   measured 385px inside a 343px deck, so it hung outside the stack it is
   supposed to sit on top of. */
.drift-surface, .drift-surface *, .drift-surface *::before, .drift-surface *::after {
  box-sizing: border-box;
}

.drift-surface {
  margin: 0;
  min-height: 100dvh;
  overflow-x: hidden;
  /* No pull-to-refresh and no rubber-banding. A downward drag near the top of
     the page was being taken by the browser before the swipe handler saw it,
     which is why up and down did nothing on a phone. */
  overscroll-behavior: none;
  background: var(--ink);
  color: var(--t0);
  font-family: var(--sans);
  font-weight: 300;
  padding:
    env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* [hidden] MUST beat our own display declarations. The UA rule is
 * [hidden] { display: none }, which any explicit `display` on the same element
 * overrides — and .drift-setup, .drift-axes, .drift-stage and the condensate
 * panel all set display: grid. Without this the setup form stays on screen
 * after the stage opens while el.hidden dutifully reports true, so the DOM and
 * the pixels disagree and only a screenshot catches it. Pinned by
 * test/drift-client-guards.test.ts. */
.drift-surface [hidden] { display: none !important; }

.drift-surface .drift-vh {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.drift-surface .drift-masthead { padding: 18px 20px 0; }

.drift-surface .drift-doors {
  display: flex;
  gap: 18px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.drift-surface .drift-doors a {
  color: var(--label);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  min-height: 44px;
  /* WCAG target size is 44 in BOTH axes. "board" is a short word and measured
   * 41px wide; only checking height hid that. */
  min-width: 44px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.drift-surface .drift-doors a:hover,
.drift-surface .drift-doors a:focus-visible { color: var(--t0); border-color: var(--pin); }

.drift-surface .drift-title {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(38px, 11vw, 64px);
  letter-spacing: -0.015em;
  margin: 8px 0 0;
}

.drift-surface .drift-setup { padding: 24px 20px; display: grid; gap: 22px; }
/* Suggested pairs. They wrap rather than scroll — a horizontal scroller at
   390px hides options behind a gesture that competes with the swipe surface
   below it. */
.drift-surface .drift-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 4px;
}
.drift-surface .drift-pill {
  min-height: 44px;
  min-width: 44px;
  padding: 0 14px;
  background: transparent;
  color: var(--t1);
  border: 1px solid var(--hair);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: lowercase;
  cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease), opacity 0.3s var(--ease);
}
.drift-surface .drift-pill:hover,
.drift-surface .drift-pill:focus-visible { color: var(--t0); border-color: var(--pin); }
.drift-surface .drift-pill[data-used='true'] { opacity: 0.4; color: var(--pin); border-color: var(--pin); }

.drift-surface .drift-legend {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--label); margin: 0 0 10px;
}
.drift-surface .drift-seed,
.drift-surface .drift-axis-row { display: flex; gap: 10px; align-items: center; }
/* MEASURED overflow: the axis step rendered 442px wide in a 390px viewport
 * (critic cycle 1, live-capture/ui/02-axes.png). Two 44pt-min inputs plus an
 * arrow plus gaps cannot fit side by side at that width, so the row wraps and
 * each pole takes a full line below 430px. */
@media (max-width: 430px) {
  .drift-surface .drift-axis-row { flex-wrap: wrap; }
  .drift-surface .drift-axis-row .drift-input { flex: 1 1 100%; }
  .drift-surface .drift-arrow {
    width: 100%; text-align: center; line-height: 1;
    /* rotate the glyph itself, not the box — a rotated full-width box overlaps
     * whatever sits beneath it in the wrapped column. */
    display: block;
  }
  .drift-surface .drift-arrow::before { content: ''; }
}
.drift-surface .drift-axes { display: grid; gap: 14px; }
 /* aria-hidden decoration. pointer-events: none is load-bearing — in the
  * wrapped layout below it spans the full row and would otherwise sit on top of
  * the submit button and swallow every click. */
.drift-surface .drift-arrow { color: var(--faint); pointer-events: none; }

.drift-surface .drift-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  padding: 10px 12px;
  background: var(--deep);
  color: var(--t0);
  border: 1px solid var(--hair);
  border-radius: 3px;
  font: inherit;
}
.drift-surface .drift-button {
  min-height: 44px;
  min-width: 44px;
  padding: 10px 18px;
  background: transparent;
  color: var(--pin);
  border: 1px solid var(--hair);
  border-radius: 3px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
}
.drift-surface .drift-status {
  font-size: 13px; line-height: 1.6; color: var(--label); margin: 0; min-height: 1.6em;
}
.drift-surface .drift-status[data-tone='warn'] { color: var(--t2); }
.drift-surface .drift-status[data-tone='degraded'] { color: var(--t2); font-style: italic; }

.drift-surface .drift-stage {
  position: relative;
  min-height: 78dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 16px;
  padding: 12px 20px 24px;
}

/* No reservation, because nothing overlaps any more: the chip is a flex sibling
 * of the gauges rather than an absolutely positioned overlay. A reserved gutter
 * has to be re-guessed every time the chip's content changes, and it was wrong
 * twice. */
.drift-surface .drift-stage-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.drift-surface .drift-gauges { flex: 1 1 auto; min-width: 0; display: grid; gap: 8px; }
.drift-surface .drift-gauge {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: center;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--label);
}
/* A degraded axis stays visibly degraded for the life of the session, not just
 * while the setup form is on screen. A bare pole scores AUC 0.640 against 0.980
 * for an expanded one, so this is a quality cliff the user must keep seeing. */
.drift-surface .drift-gauge[data-degraded='true'] { color: var(--t2); }
.drift-surface .drift-gauge[data-degraded='true'] span::after { content: ' ~'; opacity: 0.7; }

.drift-surface .drift-gauge-track {
  position: relative; height: 1px; background: var(--hair);
}
.drift-surface .drift-gauge-mark {
  position: absolute; top: -3px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--pin); transform: translateX(-50%);
  transition: left 0.3s var(--ease), top 0.3s var(--ease);
}

/* THE GAUGE MATCHES THE GESTURE. The x-axis is driven by a left/right swipe and
 * reads as a horizontal row; the y-axis is driven by an up/down swipe and so
 * stands as a column beside the deck. A horizontal row for a vertical axis has
 * no spatial relationship to the thing that moves it and has to be decoded. */
.drift-surface .drift-arena {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  align-items: stretch;
  min-height: 0;
}
.drift-surface .drift-gauge-column { display: grid; }
.drift-surface .drift-gauge-column[hidden] { display: none; }

.drift-surface .drift-gauge--y {
  grid-template-columns: none;
  grid-template-rows: auto 1fr auto;
  justify-items: center;
  gap: 12px;
}
/* Rotated rather than stacked: a column of horizontal words would need far more
   width than a 390px screen can spare next to the card. */
.drift-surface .drift-gauge--y > span {
  writing-mode: vertical-rl;
  text-orientation: mixed;
}
.drift-surface .drift-gauge--y .drift-gauge-track {
  width: 1px;
  height: auto;
  align-self: stretch;
}
/* Top is the NEGATIVE pole because swiping DOWN moves toward the positive one —
   the label order follows the gesture, not the convention that up means more. */
.drift-surface .drift-gauge--y .drift-gauge-mark {
  left: 0;
  transform: translate(-3px, -50%);
}

/* THE DECK. Two ghosts behind the card so the surface reads as a stack you can
   throw, rather than a word sitting on a background. They are offset and rotated
   a little — a perfectly square stack looks like a shadow bug, a slightly loose
   one looks like paper. */
.drift-surface .drift-deck {
  position: relative;
  align-self: center;
  justify-self: center;
  width: min(88vw, 420px);
  /* Matches .drift-card's min-height so the ghosts, at inset: 0, are exactly
     the card's box offset downward. */
  min-height: 34dvh;
  display: grid;
  place-items: center;
  /* The browser must not reserve panning on the gesture plane. Load-bearing for
     vertical swipes; see the touchmove handler in drift.js. */
  touch-action: none;
}

.drift-surface .drift-ghost {
  position: absolute;
  /* Same box as the card, only offset. Sized as a percentage inset of the DECK
     they were a larger frame AROUND the card rather than paper under it —
     visible above and below it, which read as a border bug rather than a
     stack. The deck is sized to the card below, so inset: 0 matches it. */
  inset: 0;
  border: 1px solid var(--hair);
  border-radius: 6px;
  background: var(--deep);
  pointer-events: none;
}
/* Reads off the card below, so it only becomes legible as the top card is
 * thrown clear of it. Sits above the ghosts and below the card. */
/* The bearing fills the space the card VACATES, and the arrow in its text
 * carries the direction. Two earlier placements failed: centred, it sat behind
 * the card's word; on the destination edge, the card travelled straight at it
 * and chased it down. The side a card leaves is the only region guaranteed to
 * be clear of it. */
.drift-surface .drift-bearing {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--pin);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s var(--ease);
}
.drift-surface .drift-bearing[data-active='true'] { opacity: 1; }
.drift-surface .drift-bearing[data-side='right'] { place-items: center end; padding-right: 4px; }
.drift-surface .drift-bearing[data-side='left']  { place-items: center start; padding-left: 4px; }
.drift-surface .drift-bearing[data-side='down']  { place-items: end center; padding-bottom: 6px; }
.drift-surface .drift-bearing[data-side='up']    { place-items: start center; padding-top: 6px; }

/* ONE rule per ghost. These were split across two — offsets here, animation two
   hundred lines away — and an edit that updated one and not the other left the
   ghosts rendering old offsets while the keyframes used new ones. Nothing
   failed; it just quietly drew the wrong thing. The ghosts drift on different
   periods so the stack breathes rather than sliding as one rigid block. */
.drift-surface .drift-ghost-1 {
  transform: translateY(14px) rotate(-1.3deg);
  opacity: 0.7;
  animation: drift-wander-slow 23s ease-in-out infinite;
}
.drift-surface .drift-ghost-2 {
  transform: translateY(26px) rotate(1.7deg);
  opacity: 0.4;
  animation: drift-wander-slower 31s ease-in-out infinite;
}

/* THE CARD IS A CARD. It had no box of its own — only the ghosts carried a
 * background and border — so the thing that looked like the top card was
 * actually ghost-1, and a swipe appeared to move the text off a stationary
 * card. It now has the same surface as the ghosts and sits above them. */
.drift-surface .drift-card {
  position: relative;
  z-index: 2;
  align-self: center;
  justify-self: center;
  width: 100%;
  min-height: 34dvh;
  display: grid;
  place-items: center;
  background: var(--card);
  border: 1px solid var(--card-edge);
  border-radius: 8px;
  /* Two shadows: a tight one that reads as contact with the stack, and a wide
     soft one that reads as height above it. One shadow gives you either
     "sitting on" or "floating", never both. */
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.55), 0 18px 44px rgb(0 0 0 / 0.6);
  text-align: center;
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(30px, 9vw, 54px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--t0);
  /* No background/border reset here. Both were left over from when the card was
     bare text with no surface of its own, and being LATER in the rule they beat
     the fill and edge declared above — the card rendered fully transparent, and
     what looked like a card was its shadow and border alone. */
  cursor: pointer;
  /* The card IS the primary control — tap keeps, swipe moves — so it carries a
   * real hit box. It is a div[role=button] rather than a <button>, which is
   * exactly why the earlier audit missed it. */
  /* No min-height here: the card already declares 34dvh above, and a second
     min-height later in the same rule silently won and collapsed it to a 44px
     band shorter than the ghosts it was meant to sit on. min-width stays —
     it is not duplicated. */
  min-width: 44px;
  padding: 20px 18px;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
  /* DRIFT. The whole surface is named for it, so the resting state should not
     be perfectly still — the card wanders a few pixels on a long, uneven cycle,
     the way something suspended in air does. Slow enough that you notice it
     only if you stop and look, which is the point. Suppressed entirely under
     prefers-reduced-motion. */
  animation: drift-wander 19s ease-in-out infinite;
}

@keyframes drift-wander {
  0%   { transform: translate(0, 0); }
  27%  { transform: translate(4px, -6px); }
  53%  { transform: translate(-3px, 3px); }
  78%  { transform: translate(5px, 4px); }
  100% { transform: translate(0, 0); }
}
@keyframes drift-wander-slow {
  0%   { transform: translateY(14px) rotate(-1.3deg); }
  50%  { transform: translateY(17px) rotate(-1.8deg); }
  100% { transform: translateY(14px) rotate(-1.3deg); }
}
@keyframes drift-wander-slower {
  0%   { transform: translateY(26px) rotate(1.7deg); }
  50%  { transform: translateY(30px) rotate(2.3deg); }
  100% { transform: translateY(26px) rotate(1.7deg); }
}

/* The strangeness cue. tier already ships on Served, so this costs nothing.
 * SPEC.md holds the ramp as a guardrail: pale slate -> lilac -> warm ember,
 * pinned = gold. */
.drift-surface .drift-card[data-tier='0'] { color: var(--t0); }
.drift-surface .drift-card[data-tier='1'] { color: var(--t1); }
.drift-surface .drift-card[data-tier='2'] { color: var(--t2); }
.drift-surface .drift-card[data-pinned='true'] { color: var(--pin); }
.drift-surface .drift-card[data-leaving='true'] { opacity: 0; }
/* While a thumb is on it the card follows the finger, so the idle wander has to
   get out of the way or the two transforms fight. */
.drift-surface .drift-card[style*='translate'] { animation: none; transition: none; }

.drift-surface .drift-condensate {
  flex: 0 0 auto;
  min-width: 44px; min-height: 44px;
  background: transparent;
  border: 1px solid var(--hair);
  border-radius: 999px;
  color: var(--pin);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
/* A bare circled number says nothing, and says it hardest at zero. The label
 * makes the chip self-describing without a legend, and the drop reads at a
 * glance. Critic cycle 1, minor. */
.drift-surface .drift-condensate::before { content: '\25CF'; font-size: 8px; opacity: 0.8; }
.drift-surface .drift-condensate::after { content: 'kept'; opacity: 0.75; }
.drift-surface .drift-condensate-panel {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--deep);
  border: 1px solid var(--hair);
  border-radius: 4px;
  padding: 20px;
  overflow-y: auto;
  display: grid;
  gap: 10px;
  align-content: start;
}
.drift-surface .drift-condensate-item { color: var(--pin); font-size: 16px; }

.drift-surface .drift-edge {
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--t2);
  margin: 0;
}
/* The hint is the ONLY thing that teaches the gesture, and it was drawn in
 * --faint, which measures 2.69:1 against the ground — far under the 4.5:1 floor
 * for text this size. --faint is a decorative token in the night-walk palette
 * (hairlines, the inert arrow glyph); using it for the surface's only
 * instruction was a misuse of it, not a problem with it. --label measures
 * 6.88:1. Pinned by test/contrast.test.ts, which did not cover this surface at
 * all until now. */
.drift-surface .drift-hint {
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--label);
  margin: 0;
}

/* Fade ONLY, no drift — a correctness constraint, not a preference.
 *
 * This is a crossfade, not an absence of motion. The earlier version set
 * `transition: none`, which removes the fade as well as the movement and makes
 * cards snap — the opposite of what the guardrail asks for. Opacity keeps
 * transitioning; transform does not, and nothing animates. Critic cycle 1. */
@media (prefers-reduced-motion: reduce) {
  /* The drag itself is direct manipulation — the card is under the thumb, and
     removing that would break the gesture rather than calm it. What goes is the
     IDLE wander and the decorative tilt; the inline translate a drag sets is
     left alone, and the rule below is scoped so it cannot fight it. */
  .drift-surface .drift-card:not([style*='translate']) {
    animation: none;
    transition: opacity 0.3s var(--ease);
    transform: none !important;
  }
  .drift-surface .drift-bearing { transition: none; }
  /* The ghosts keep their offsets — that is layout, and the stack must still
     read as a stack — but they stop moving. */
  .drift-surface .drift-ghost-1 { animation: none; transform: translateY(14px) rotate(-1.3deg); }
  .drift-surface .drift-ghost-2 { animation: none; transform: translateY(26px) rotate(1.7deg); }
  .drift-surface .drift-gauge-mark {
    animation: none;
    transition: none;
  }
}
