/* echoswarm.app
 *
 * Every colour comes from assets/tokens.css, which build.py generates from
 * web/graph/src/themes.js. There are no hex values in this file on purpose:
 * BRAND.md §5 makes themes.js the single source for the palette, and the
 * version of this stylesheet that carried its own :root had already drifted
 * (--ink #14161f against the theme's #0D1020).
 *
 * Direction per docs/WEB_STRATEGY.md §4: one paper substrate, 1px hairlines
 * instead of cards, an asymmetric grid with a mono label in the left gutter,
 * everything left-aligned on one measure.
 *
 * Typography is a system stack for now. The direction calls for self-hosted
 * subset Poppins / IBM Plex Mono / Source Serif 4 (~95KB, 5 files); pointing
 * at woff2 files that do not exist yet would fall back silently, which is
 * worse than declaring the stack honestly.
 */

:root {
  /* Line length. `ch` ignores the viewport, so every use below is wrapped in
     min(…, 100%) — without that the text simply overflows and clips on a
     phone, which is exactly what happened the first time this was rendered. */
  --measure: min(62ch, 100%);
  --measure-tight: min(44ch, 100%);
  /* Wide enough for a 17-character mono label plus the column gap below.
     Measured: "02 / HOW IT WORKS" is the longest label on the site. */
  --gutter: 148px;
  --sans: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --serif: "Source Serif 4", Georgia, "Times New Roman", serif;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 1.0625rem/1.62 var(--sans);
  -webkit-text-size-adjust: 100%;
}

/* ---------- shell ---------- */

.shell-header {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  padding: 18px 24px;
  border-bottom: 1px solid var(--hairline);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

/* The wordmark's second half carries the accent. Flat, never a gradient —
   the gradient belongs to the mark only (BRAND.md §5). */
.brand b { font-weight: 600; color: var(--accent); }

.shell-header nav a {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--label);
  text-decoration: none;
  margin-left: 20px;
  /* 44px touch target without moving the baseline */
  padding: 14px 0;
}

.shell-header nav a:hover,
.brand:hover b { color: var(--accent); }

main {
  max-width: 900px;
  margin: 0 auto;
  padding: 56px 24px 88px;
}

.shell-footer {
  border-top: 1px solid var(--hairline);
  padding: 24px;
  color: var(--secondary);
  font-size: 0.875rem;
}

.shell-footer p { margin: 6px 0; max-width: var(--measure); }
.shell-footer a { color: var(--secondary); }
.shell-footer a:hover { color: var(--accent); }
.shell-lang a { color: var(--accent); }

/* ---------- the asymmetric grid ---------- */

section {
  display: grid;
  grid-template-columns: var(--gutter) minmax(0, 1fr);
  /* Without this the label's last character sits against the headline's first
     one and the two read as a single run of text. */
  column-gap: 22px;
  /* Deliberately unequal section rhythm — even padding reads as a template. */
  padding-block: 0 72px;
}

section > * { grid-column: 2; }

/* The mono gutter label is the wayfinding layer. Below 900px it stops being
   a margin note and becomes the section heading, so it keeps its size.
   Budget: it must fit --gutter on one line, which at this size is about
   16 characters. A longer label wraps, and the second line lands beside the
   body copy where it reads as part of the paragraph. Keep labels short. */
.margin-label {
  grid-column: 1;
  grid-row: span 99;
  align-self: start;
  margin: 0.4rem 0 0;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.8125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--label);
}

/* ---------- type ---------- */

h1 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(1.5rem, 2.8vw, 2rem);
  line-height: 1.14;
  letter-spacing: -0.016em;
  margin: 0 0 14px;
  max-width: var(--measure-tight);
}

h2 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 1.1875rem;
  letter-spacing: -0.01em;
  margin: 34px 0 8px;
}

h3 { font-size: 1.0625rem; font-weight: 600; margin: 24px 0 6px; }

p { margin: 0 0 14px; max-width: var(--measure); }

.deck {
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--secondary);
}

/* The machine record: counters, specs, timestamps. Tabular so columns of
   numbers line up. */
.spec {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  color: var(--label);
  font-variant-numeric: tabular-nums;
}

.meta {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--secondary);
  margin-bottom: 26px;
}

a { color: var(--accent); }

ul, ol { padding-left: 20px; max-width: var(--measure); }
li { margin: 4px 0; }

code {
  font-family: var(--mono);
  font-size: 0.9em;
  color: var(--label);
}

/* ---------- hairlines instead of cards ---------- */

/* The legal pages use .card for their summary block. One hairline box, no
   shadow, no radius flourish — the two shadows the direction allows are
   reserved for the caption chip and the verdict sheet. */
.card {
  border: 1px solid var(--hairline);
  border-radius: 10px;
  padding: 18px 20px;
  margin: 22px 0;
  background: var(--sheet);
  max-width: var(--measure);
}

.card h2 { margin-top: 0; }
.card p:last-child { margin-bottom: 0; }

table {
  width: 100%;
  max-width: none;
  border-collapse: collapse;
  margin: 14px 0;
  font-size: 0.9375rem;
}

th, td {
  border-bottom: 1px solid var(--hairline);
  border-top: 0;
  border-left: 0;
  border-right: 0;
  padding: 10px 12px 10px 0;
  text-align: left;
  vertical-align: top;
  /* Defensive, not a fix for anything observed. Turkish is agglutinative and
     its ordinary words run long — the privacy tables carry
     "işaretlediğinizde" and "değerlendirmemizi" — and a word wider than its
     column would set that column's minimum width and push the page sideways.
     Measured on the Turkish pages at 375px: it does not happen today. It
     costs nothing to hold the door shut, and the rule is inert in English
     and in every language whose words stay short. */
  overflow-wrap: break-word;
}

th {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--label);
}

.num { text-align: right; font-variant-numeric: tabular-nums; }

/* A table is the one element allowed to break the measure, so it needs its own
   scroll container rather than pushing the page sideways. */
.scroll-x { overflow-x: auto; margin: 14px 0; }
.scroll-x table { margin: 0; }

/* ---------- the record: transcript beside the minimap ---------- */

/* The question is set larger than the body but smaller than the verdict: it is
   what was asked, and the answer outranks it. */
.question {
  font-family: var(--serif);
  font-size: 1.3125rem;
  line-height: 1.4;
  margin: 26px 0 16px;
  max-width: var(--measure);
}

.question::before {
  content: "";
  display: block;
  width: 34px;
  border-top: 2px solid var(--accent);
  margin-bottom: 12px;
}

/* ---------- the graph plate ---------- */

/* A dark instrument panel set into a paper page. Two reasons it is dark rather
   than matching the page: the node glow and the travelling impulse are additive
   light, which does nothing over #F7F8FC, and the app itself runs on navy — so
   the plate shows the product's own surface rather than a washed-out imitation
   of it. Its colours come from DARK_THEME via generated tokens. */
.panel {
  margin: 0 0 4px;
  padding: 0;
  border-radius: 6px;
  overflow: hidden;
  background: var(--panel-bg);
  box-shadow: var(--shadow);
}

.panel-canvas {
  display: block;
  width: 100%;
  height: auto;
  /* Wide and short: the graph is a diagram in a column of text, not a stage. */
  aspect-ratio: 22 / 10;
}

.panel-hud {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 20px;
  margin: 0;
  padding: 9px 14px 11px;
  border-top: 1px solid rgb(255 255 255 / 8%);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--panel-hud);
  font-variant-numeric: tabular-nums;
}

.panel-hud b { font-weight: 500; color: var(--panel-hud-value); }

.panel-live { display: inline-flex; align-items: center; gap: 7px; }

/* Capped and scrolled rather than pushing the verdict off the screen as turns
   append. The verdict has to stay visible while the room fills in — that is
   the whole argument of the hero. */
.transcript {
  max-height: 21rem;
  overflow-y: auto;
  padding-right: 12px;
  /* --wire, not --hairline: this rule is the spine of the record and has to be
     visible against the paper. --hairline is for table rules and box edges. */
  border-left: 1px solid var(--wire);
  padding-left: 18px;
  /* The gap matters: without it the clipped last line sits flush against the
     verdict sheet and reads as two boxes colliding. */
  margin: 18px 0 22px;
  scrollbar-width: thin;
}

/* Both cut edges get faded, so a scrolled box reads as scrolled rather than
   broken. The two fade depths are custom properties so the four states compose
   from two independent rules instead of needing a rule per combination; a
   depth of 0 collapses that end's gradient into a hard stop, i.e. no fade. */
.transcript {
  --fade-top: 0px;
  --fade-bottom: 26px;
  mask-image: linear-gradient(to bottom,
    transparent 0, var(--paper) var(--fade-top),
    var(--paper) calc(100% - var(--fade-bottom)), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, var(--paper) var(--fade-top),
    var(--paper) calc(100% - var(--fade-bottom)), transparent 100%);
}

/* Set by replay.js only when scrollTop > 0, so an unscrolled transcript keeps
   its first line at full contrast. */
.transcript[data-scrolled] { --fade-top: 24px; }

/* At the end of the scroll there is nothing more to hint at, so stop dimming
   the final line of the argument. */
.transcript[data-at-end] { --fade-bottom: 0px; }

.transcript-full {
  max-height: none;
  overflow: visible;
  margin-top: 0;
  mask-image: none;
  -webkit-mask-image: none;
}

.line {
  margin: 0 0 14px;
  max-width: var(--measure);
  font-size: 1rem;
  line-height: 1.55;
}

.line:last-child { margin-bottom: 0; }

/* The speaker sits on its own line as a mono label — the name is metadata
   about the utterance, not part of it. */
.speaker {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--label);
  margin-bottom: 3px;
}

.said { display: block; }

/* The one place agent colour appears in text: a 6px dot that matches the
   node's colour on the minimap and in the app's graph. Colour is never the
   only signal — the name is right beside it. */
.hue {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--wire);
}

.hue-1 { background: var(--agent-1); }
.hue-2 { background: var(--agent-2); }
.hue-3 { background: var(--agent-3); }
.hue-4 { background: var(--agent-4); }

/* Lit only while turns are actually arriving. A permanently pulsing "live"
   indicator is a lie about a page that finished loading. */
.dot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--panel-edge);
  transition: background 200ms var(--ease-out), box-shadow 200ms var(--ease-out);
}

.dot[data-on] {
  background: var(--panel-live);
  box-shadow: 0 0 6px var(--panel-live);
}

/* ---------- the verdict ---------- */

/* The largest type on the page, and the only element with a shadow. Everything
   above it is the argument; this is what it was for. */
.verdict {
  max-width: var(--measure);
  padding: 20px 22px;
  background: var(--sheet);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--accent);
  border-radius: 3px;
  /* --shadow is a whole box-shadow value, not a colour: it is the app card's
     own shadow from themes.js, so the verdict sheet on the web lifts off the
     paper by exactly as much as a card does in the app. */
  box-shadow: var(--shadow);
  margin: 0 0 18px;
}

.verdict-label {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: var(--label);
  margin: 0 0 8px;
}

/* Larger than the h1 on purpose. The headline says what the product is; this
   says what the room decided, and the direction puts the decision at the top
   of the type hierarchy. Measured: 38px here against the h1's 32px. */
.verdict-text {
  font-family: var(--serif);
  font-size: clamp(1.5rem, 3.6vw, 2.375rem);
  line-height: 1.22;
  letter-spacing: -0.012em;
  margin: 0;
  max-width: none;
}

.verdict-more {
  margin: 12px 0 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
}

.summary { color: var(--secondary); }

/* ---------- replay control ---------- */

.replay-row { margin: 0; min-height: 44px; display: flex; align-items: center; }

/* A text button, not a pill: the page has no other buttons and inventing a
   filled CTA for "watch it again" would overstate it. */
.text-button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  padding: 12px 2px;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
}

.text-button:hover { border-bottom-color: var(--accent); }

/* ---------- room pages ---------- */

.moments { max-width: var(--measure); }
.moments li { margin: 8px 0; }

.note {
  font-size: 0.875rem;
  color: var(--secondary);
  border-left: 2px solid var(--wire);
  padding-left: 12px;
}

/* ---------- skip link ---------- */

.skip {
  position: absolute;
  left: -9999px;
  font-family: var(--mono);
  font-size: 0.8125rem;
}

.skip:focus {
  left: 8px;
  top: 8px;
  z-index: 9;
  background: var(--paper);
  border: 1px solid var(--accent);
  padding: 10px 14px;
  border-radius: 3px;
}

/* ---------- scaffold state ---------- */

/* Visible on purpose: an unbuilt surface should look unbuilt, not finished. */
.placeholder {
  border: 1px dashed var(--wire);
  border-radius: 8px;
  padding: 18px 20px;
  color: var(--secondary);
  font-size: 0.9375rem;
}

.hero-room { margin: 26px 0 0; }

/* ---------- focus and motion ---------- */

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

a { transition: color 160ms var(--ease-out); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* ---------- narrow ---------- */

@media (max-width: 900px) {
  section {
    grid-template-columns: minmax(0, 1fr);
    padding-block: 0 56px;
  }
  section > *, .margin-label { grid-column: 1; }
  .margin-label { grid-row: auto; margin-bottom: 10px; }
  main { padding: 36px 20px 64px; }
  .shell-header { flex-direction: column; align-items: flex-start; gap: 4px; }
  .shell-header nav a { margin: 0 20px 0 0; }

  /* Squarer on a phone: at 22:10 a 350px-wide plate is 160px tall, which
     crushes the graph into a band. */
  .panel-canvas { aspect-ratio: 14 / 10; }
  /* Tighter gaps, but not smaller text — 12px is the floor, and the HUD wraps
     rather than shrinking below it. */
  .panel-hud { gap: 4px 14px; padding: 9px 12px 11px; }
  .transcript { max-height: 24rem; padding-right: 4px; }
}

/* ---------- the ask form ---------- */

/* The one thing on this page that is not reading. It sits directly under the
   hero because the moment someone finishes a transcript is the moment they
   know whether they want one of their own. */
.ask {
  max-width: var(--measure);
  margin: 22px 0 0;
  padding: 20px 22px;
  background: var(--sheet);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--accent);
  border-radius: 3px;
}

.ask-field { display: block; margin-bottom: 14px; }

.ask-field > span {
  display: block;
  margin-bottom: 6px;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--label);
}

.ask textarea,
.ask input[type="email"] {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--wire);
  border-radius: 3px;
  resize: vertical;
}

.ask textarea:focus,
.ask input[type="email"]:focus { border-color: var(--accent); }

/* The honeypot. Off-screen rather than display:none — some bots skip anything
   that is not rendered, and aria-hidden plus tabindex keeps it away from
   screen readers and the keyboard. */
.ask-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.ask-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 4px;
}

/* The only filled button on the site. It gets to be one: it is the single
   action, and everything else is a link. */
.ask button {
  appearance: none;
  border: 0;
  border-radius: 3px;
  padding: 12px 22px;
  min-height: 44px;
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sheet);
  background: var(--accent);
  cursor: pointer;
  transition: opacity 160ms var(--ease-out);
}

.ask button:hover { opacity: 0.88; }
.ask button:disabled { opacity: 0.5; cursor: default; }

.ask-status { margin: 0; font-size: 0.9375rem; }
.ask-status[data-kind="error"] { color: var(--accent); }
.ask-status[data-kind="ok"] { color: var(--live); }

.ask-note {
  margin: 14px 0 0;
  font-size: 0.875rem;
  color: var(--secondary);
}

/* Once it has been sent there is nothing left to fill in; leaving the fields
   live invites a second identical submission. */
.ask[data-sent] .ask-field,
.ask[data-sent] .ask-actions button { display: none; }

/* ---------- store actions ---------- */

/* The page's primary action, now that there is something to install.
   Before the Play listing went live the email form was the only thing anyone
   could do here; "download it" outranks "tell me later" the moment the app
   exists, which is why the badge sits above the form rather than beside it. */
.store-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin: 24px 0 0;
  max-width: var(--measure);
}

/* Google's badge, shown at its own proportions. Height drives the size —
   the guidelines set a minimum height, not a width — and `width: auto` keeps
   the intrinsic 646x250 ratio while the width/height attributes on the img
   still reserve the box, so nothing reflows when it loads.

   No hover opacity and no filter: altering the artwork is the one thing the
   brand rules actually forbid. The link gets its feedback from the focus
   ring and the cursor. */
.store-badge {
  display: inline-flex;
  /* Cancels the inline-image baseline gap, which otherwise reads as the
     badge sitting a few pixels above the text beside it. */
  line-height: 0;
}

.store-badge img {
  height: 56px;
  width: auto;
}

/* A fact, not a disabled button. Styling iOS as a greyed-out control invites
   a tap and answers it with nothing, which reads as a bug rather than as a
   release schedule. */
.store-soon {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--secondary);
}

.store-note {
  margin: 18px 0 0;
  max-width: var(--measure);
  font-size: 0.9375rem;
  color: var(--secondary);
}

/* ---------- the screenshot strip ---------- */

/* Scrolls inside itself. Five phones at any readable width exceed the
   measure, and the alternative — shrinking them until they fit — produces
   five illegible thumbnails, which is worse than showing three and letting
   the reader push. */
.shots {
  display: flex;
  gap: 18px;
  margin: 22px 0 26px;
  padding: 0 0 14px;
  list-style: none;
  /* Stated, not inherited. `ul, ol` caps every list at --measure, which is
     right for prose and is also what silently clamped this strip. Keeping the
     measure on the desktop layout is deliberate — the strip should end where
     the paragraphs end — but it has to be said here, or the mobile override
     below fights a rule nobody remembers writing. */
  max-width: var(--measure);
  overflow-x: auto;
  scrollbar-width: thin;
  /* The cut edge should read as "there is more", not as a crop bug. */
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
}

.shots > li {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

.shots figure { margin: 0; }

/* A hairline, not a shadow. The design has exactly two raised surfaces and
   this is not one of them; the device already has its own frame in the
   pixels, so a second frame in CSS would be a frame around a frame. */
.shots img {
  display: block;
  width: 210px;
  height: auto;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: var(--sheet);
}

.shots figcaption {
  margin-top: 10px;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--label);
}

@media (max-width: 900px) {
  /* Full-bleed on a phone: gutters on a horizontal scroller waste the only
     axis it has. The negative margin matches main's 20px padding.
     The explicit width is not redundant — as a stretched grid item the strip
     sizes to its track and ignores the negative margins, which bled the left
     edge and left the right one short. */
  .shots {
    max-width: none;
    width: calc(100% + 40px);
    margin-inline: -20px;
    padding-inline: 20px;
    scroll-padding-left: 20px;
  }
}
