/* Ivy. One accent color, the iOS feel (CONTRACT 13), light and dark, the system font. */
/* 0.2.0: replaced the warm light theme of 0.1.x (bg #fbf6ef, surface-2 #f4ece1, line #e6dccf,
   14 px corners, 1.5 px borders) with the iPadOS 26 tokens below, on Dawson's GO for the iOS feel.
   The layout, the three breakpoints of section 11 and 100dvh are unchanged. */

:root {
  /* web 0.3.0 palette, pass 3 of the design loop (2026-10-26): "warm daylight, teal". The pass 2
     ground a touch brighter and less yellow, the accent moved from forest green to the orb's teal,
     her bubble in the orb's mint, the iOS 26 glass lift (fainter, wider shadows).
     Pass 1 ("sea glass", a mint ground) scored 6 on warmth, and pass 2 ("warm daylight", a forest
     green accent) scored 7. Their values and those of web 0.2.1 are kept in each comment. */
  --bg: #f8f7f4;                          /* page, warm daylight; pass 2 #f8f6f1; pass 1 #eef8f4; 0.2.1 #f2f2f7 grouped gray */
  --surface: #ffffff;                     /* grouped inset card; unchanged */
  --fill: #efede8;                        /* iOS tertiary fill, warmed: chips, fields; pass 1 rgba(15, 125, 108, 0.08); 0.2.1 rgba(118, 118, 128, 0.12) */
  --fill-strong: #e5e2dc;                 /* iOS secondary fill, warmed: badges, the X, pressed; pass 1 rgba(15, 125, 108, 0.15); 0.2.1 rgba(118, 118, 128, 0.2) */
  --her-bubble: #e3f3ee;                  /* new in pass 3: her chat bubble in the orb's mint; passes 1 and 2 and 0.2.1 used --fill */
  --ink: #1d1b18;                         /* text, a warm near black; pass 1 #14211e; 0.2.1 #000000 */
  --muted: #5e5953;                       /* secondary text, warm gray; pass 1 #586663; 0.2.1 rgba(60, 60, 67, 0.64) */
  --line: #d9d6cf;                        /* hairline separator; pass 1 rgba(20, 90, 78, 0.2); 0.2.1 rgba(60, 60, 67, 0.29) */
  --ghost-edge: #878178;                  /* new in pass 3: the dashed edge of the ghost button, 3:1 or better; passes 1 and 2 and 0.2.1 used --line */
  --accent: #0b7a70;                      /* the one accent, the orb's teal: "your turn"; pass 2 #107a5b; pass 1 #0f7d6c; 0.2.1 #1d7a6b */
  --accent-ink: #ffffff;                  /* unchanged */
  --accent-ink-soft: #ffffff;             /* label on the lit chip; pass 1 rgba(255, 255, 255, 0.94); 0.2.1 rgba(255, 255, 255, 0.86) */
  --accent-text: #086f68;                 /* the accent as text on the page; pass 2 #0e7458; pass 1 #0b6f60; 0.2.1 #1d7a6b */
  --accent-soft: rgba(11, 122, 112, 0.12); /* pass 2 rgba(16, 122, 91, 0.12); pass 1 rgba(15, 125, 108, 0.12); 0.2.1 rgba(29, 122, 107, 0.14) */
  --glass: rgba(255, 255, 255, 0.86);     /* tinted glass buttons on the page; pass 1 0.82; 0.2.1 rgba(255, 255, 255, 0.72) */
  --glass-edge: rgba(70, 55, 35, 0.1);    /* pass 2 rgba(70, 55, 35, 0.08); pass 1 rgba(15, 90, 78, 0.1); 0.2.1 rgba(0, 0, 0, 0.06) */
  --bar: rgba(255, 255, 255, 0.8);        /* frosted top bar, chat bar, nav bar; pass 1 0.78; 0.2.1 rgba(249, 249, 249, 0.74) */
  --float: 0 8px 28px rgba(70, 50, 20, 0.05), 0 0.5px 1px rgba(70, 50, 20, 0.06); /* pass 2 0 6px 24px rgba(70, 50, 20, 0.08), 0 1px 2px rgba(70, 50, 20, 0.06); 0.2.1 black at 0.07 and 0.05 */
  --btn-shadow: 0 0.5px 1.5px rgba(70, 50, 20, 0.06); /* new in 0.3.0; pass 2 0 1px 3px rgba(70, 50, 20, 0.08); 0.2.1 rgba(0, 0, 0, 0.06) freehand in .btn */
  --orb-glow: rgba(46, 158, 117, 0);      /* extra glow behind the orb: none in light; 0.2.1 rgba(29, 122, 107, 0) */
  --orb-hi: #a8e6f0;                      /* start orb, the live orb's own idle stops (orb.js hue 168); pass 1 and 0.2.1 #c9f0e6 */
  --orb-mid: #69d3be;                     /* pass 1 and 0.2.1 #5fb8a6 */
  --orb-lo: #2e9e75;                      /* pass 1 and 0.2.1 #1d7a6b */
  --start-glow: 0 0 56px rgba(46, 158, 117, 0.36); /* pass 2 0 0 48px rgba(46, 158, 117, 0.32); pass 1 and 0.2.1 0 0 48px rgba(29, 122, 107, 0.28) */
  --radius-card: 22px;
  --radius-chip: 16px;
  --pill: 999px;
  --hair: 0.5px;
  --gutter: 16px;
  --font: -apple-system, system-ui, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-round: ui-rounded, "SF Pro Rounded", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --spring: cubic-bezier(0.32, 0.72, 0, 1); /* the one curve for state changes */
  --dur: 320ms;
  /* web 0.4.0, waiting motion (CONTRACT 15). Beside the two above, which they leave as they were.
     The orb is the ceiling: its thinking pulse is a 2.4 s cycle at a scale of 0.022, so nothing
     here cycles faster than 2.4 s or swells more than 0.022. */
  --wait-breath: 2400ms;                  /* one breath of a waiting dot, a busy button, the start orb */
  --wait-sheen-dur: calc(var(--wait-breath) * 1.5); /* one pass of the sheen, slower again; pass 3 follows the breath, was 3600ms */
  --wait-ease: cubic-bezier(0.37, 0, 0.63, 1); /* ease in and out, a sine: no snap at either end */
  --wait-swell: 1.022;                    /* the start orb's breath, the orb's own thinking scale */
  --wait-sheen: #faf9f6;                  /* the light that passes over a placeholder, a touch above --fill */
  --wait-tint: var(--accent-soft);        /* the glow a busy button breathes */
  /* Pass 2 of the "keep it moving" loop: the amounts the keyframes use, so none is a bare number. */
  --wait-dim: 0.45;                       /* a waiting dot at its faintest; pass 1 0.3 with a scale of 0.8 */
  --wait-lit: 0.85;                       /* a waiting dot at its brightest; pass 1 0.9 with a scale of 1 */
  --wait-fade-low: 0.55;                  /* Reduce Motion's pulse, and the mic hint's */
  --wait-fade-soft-low: 0.78;             /* the start orb's pulse under Reduce Motion */
  --wait-edge: color-mix(in srgb, var(--accent) 34%, transparent); /* the ring a busy control breathes at its edge */
  --wait-room: calc(var(--app-h, 100dvh) * 0.28); /* the exchange card's room, its own max-height */
  /* Pass 3: the last bare numbers in the wait rules. Every wait duration is --wait-breath or follows
     it, so a new ceiling is one line: --wait-breath above. */
  --wait-ring: 1.5px;                     /* the width of the ring a busy control breathes at its edge */
  --wait-glow-blur: 14px;                 /* the blur of the soft glow inside that ring */
  --wait-dot: 8px;                        /* a waiting dot's size */
  --wait-dot-gap: 0.35em;                 /* the gap between words and the dot after them */
  --wait-problem-room: 2lh;               /* the board's problem line in a session on a phone: two lines, its clamp */
  --app-h: 100dvh;
  /* web 0.2.1, Dawson 2026-10-26: bright always. Was "light dark"; the dark tokens below
     stay on file for the design loop and never apply until he says so. */
  color-scheme: light;
}

/* web 0.2.1: parked. Was @media (prefers-color-scheme: dark); min-width 99999px never matches. */
@media (prefers-color-scheme: dark) and (min-width: 99999px) {
  :root {
    --bg: #000000;
    --surface: #1c1c1e;
    --fill: rgba(118, 118, 128, 0.24);
    --fill-strong: rgba(118, 118, 128, 0.36);
    --ink: #ffffff;
    --muted: rgba(235, 235, 245, 0.62);
    --line: rgba(84, 84, 88, 0.65);
    --accent-text: #4fc1ac;
    --accent-soft: rgba(79, 193, 172, 0.18);
    --glass: rgba(58, 58, 60, 0.6);
    --glass-edge: rgba(255, 255, 255, 0.1);
    --bar: rgba(28, 28, 30, 0.72);
    --float: 0 6px 24px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4);
    --orb-glow: rgba(64, 196, 172, 0.3);  /* the orb glows stronger in dark */
    --start-glow: 0 0 72px rgba(64, 196, 172, 0.55);
  }
}

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: var(--bg); }
html, body { margin: 0; background: var(--bg); color: var(--ink); }
body {
  font: 17px/1.3 var(--font);
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ---------- glass ---------- */
.glass-bar {
  background: var(--bar);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border: var(--hair) solid var(--glass-edge);
  box-shadow: var(--float);
}

/* ---------- controls: iOS pills ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 50px; min-width: 44px; padding: 0 22px;
  border: var(--hair) solid var(--glass-edge); border-radius: var(--pill);
  background: var(--glass); color: var(--ink);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  box-shadow: var(--btn-shadow); /* web 0.3.0: was 0 1px 3px rgba(0, 0, 0, 0.06) */
  font: 600 17px/1.2 var(--font);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: background-color var(--dur) var(--spring), color var(--dur) var(--spring);
}
.btn:hover { background: var(--fill-strong); }
.btn:active { background: var(--fill-strong); }
.btn:focus-visible, .link:focus-visible, .orb-btn:focus-visible, .chat-input:focus-visible,
.row:focus-visible, .pill-btn:focus-visible, .search-input:focus-visible, .rename-input:focus-visible {
  outline: 3px solid var(--accent); outline-offset: 2px;
}
/* Filled with the accent: Send and the check only (CONTRACT 13). */
.btn-accent { background: var(--accent); border-color: transparent; color: var(--accent-ink); box-shadow: none; }
.btn-accent:hover, .btn-accent:active { background: var(--accent); filter: brightness(1.08); }
/* web 0.3.0 pass 3: the dashed edge was var(--line); now var(--ghost-edge), 3:1 on the page. */
.btn-ghost { background: transparent; border: var(--hair) dashed var(--ghost-edge); box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
.btn-small { min-height: 44px; padding: 0 16px; font-size: 15px; }
.btn[disabled] { opacity: 0.5; cursor: default; }
.btn[aria-pressed="true"] { background: var(--fill-strong); }

.file-btn { position: relative; overflow: hidden; }
.file-btn input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; font-size: 16px;
}
.icon-btn { padding: 0; width: 50px; }
.btn-small.icon-btn { width: 44px; }

.link {
  min-height: 44px; padding: 0 8px; border: 0; background: none;
  color: var(--accent-text); font: 15px var(--font); cursor: pointer;
}
.link[disabled] { cursor: default; opacity: 0.75; }
.soon {
  display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: var(--pill);
  background: var(--fill); font-size: 13px;
}
.muted { color: var(--muted); }

/* ---------- screens ---------- */
.screen {
  width: 100%; max-width: 900px; margin: 0 auto;
  padding:
    max(12px, env(safe-area-inset-top))
    max(var(--gutter), env(safe-area-inset-right))
    max(12px, env(safe-area-inset-bottom))
    max(var(--gutter), env(safe-area-inset-left));
}

/* Start */
.start {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 20px; min-height: 100vh; min-height: 100dvh; max-width: 520px; text-align: center;
}
.start-orb {
  width: 112px; height: 112px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--orb-hi) 0%, var(--orb-mid) 45%, var(--orb-lo) 100%); /* web 0.3.0: was the freehand gradient #c9f0e6, #5fb8a6, #1d7a6b; now tokens */
  box-shadow: var(--start-glow);
}
/* The start line is the large title: 34, rounded (CONTRACT 13). */
.ivy-says { margin: 0; font: 700 34px/1.15 var(--font-round); letter-spacing: 0.01em; }
.stack { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.stack .btn { width: 100%; }
.start-foot { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.fineprint { margin: 0; color: var(--muted); font-size: 15px; }

/* Session */
.session {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr) auto auto auto;
  gap: 10px;
  height: 100vh;
  height: var(--app-h, 100dvh);
  overflow: hidden;
}
/* The top bar floats as frosted glass (CONTRACT 13). */
.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px; border-radius: var(--pill);
  position: relative; z-index: 2;
}
.status { color: var(--muted); font-size: 15px; text-align: center; flex: 1; min-width: 0; }

/* Board: a grouped inset card; the seven slots are a row of chips */
.board {
  background: var(--surface); border-radius: var(--radius-card);
  padding: 14px 16px; min-width: 0;
}
.board-problem { margin: 0 0 4px; font-size: 17px; font-weight: 600; }
.board-step { margin: 0 0 10px; color: var(--muted); font-size: 13px; }
/* 0.2.0: the slots were a seven-column grid of bordered boxes; now a horizontal chip row (CONTRACT 13). */
.board-slots {
  list-style: none; margin: 0; padding: 0;
  position: relative;
  display: flex; gap: 8px;
  overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.board-slots::-webkit-scrollbar { display: none; }
.slot {
  flex: 1 1 0; min-width: 0; min-height: 44px;
  padding: 8px 12px; border-radius: var(--radius-chip);
  background: var(--fill); color: var(--ink);
  display: flex; flex-direction: column; gap: 2px;
  transition: background-color var(--dur) var(--spring), color var(--dur) var(--spring);
}
.slot-label { font-size: 13px; color: var(--muted); font-weight: 600; letter-spacing: 0.01em; white-space: nowrap; }
.slot-value { font-size: 15px; overflow-wrap: break-word; } /* 0.2.0: was "anywhere", which split words mid-word in narrow chips */
.slot.done .slot-label::after { content: " \2713"; color: var(--accent-text); }
.slot.lit { background: var(--accent); color: var(--accent-ink); }
.slot.lit .slot-label { color: var(--accent-ink-soft); } /* web 0.3.0: was rgba(255, 255, 255, 0.86) */
.slot .katex { font-size: 1em; }

/* Orb */
.orb-wrap {
  container-type: size;
  min-height: 170px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
}
.orb-btn {
  /* 0.2.0: the room for the pill and the hint is taken out of the height; was 100cqh - 34px. The pill
     tucks 22 px into the empty glow under the blob, so the orb gives up as little as it can. */
  width: min(60vw, 32vh, 420px);
  width: min(calc(100cqh - 74px), 100cqw, 440px);
  aspect-ratio: 1 / 1; padding: 0; border: 0; border-radius: 50%;
  background: radial-gradient(closest-side, var(--orb-glow), transparent);
  cursor: pointer; flex: none;
  touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
.orb { display: block; width: 100%; height: 100%; pointer-events: none; }
.orb-hint { margin: 0; color: var(--muted); font-size: 15px; min-height: 20px; text-align: center; }
.session[data-phase="idle"] .orb-hint { color: var(--accent-text); font-weight: 600; }

/* The talk pill (CONTRACT 12): X, live bars, check. Its room is kept, so nothing jumps when it shows. */
.talk-pill {
  flex: none;
  position: relative; z-index: 1; margin-top: -22px;
  display: flex; align-items: center; gap: 10px;
  height: 56px; padding: 6px; border-radius: var(--pill);
  width: min(100%, 340px);
  background: var(--bar);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border: var(--hair) solid var(--glass-edge);
  box-shadow: var(--float);
  /* It rises like a sheet on the one spring. */
  opacity: 0; visibility: hidden; transform: translateY(14px) scale(0.98);
  transition: transform var(--dur) var(--spring), opacity var(--dur) var(--spring), visibility 0s linear var(--dur);
}
.talk-pill.show {
  opacity: 1; visibility: visible; transform: none;
  transition: transform var(--dur) var(--spring), opacity var(--dur) var(--spring), visibility 0s;
}
.pill-btn {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; padding: 0; cursor: pointer; -webkit-tap-highlight-color: transparent;
  background: var(--fill-strong); color: var(--ink);
  transition: opacity var(--dur) var(--spring);
}
.pill-accent { background: var(--accent); color: var(--accent-ink); }
/* During a plain hold the hold owns the pointer: the buttons show, dimmed, and take no taps. */
.talk-pill:not(.locked) .pill-btn { pointer-events: none; opacity: 0.45; }
.talk-bars {
  flex: 1; min-width: 0; height: 30px;
  display: flex; align-items: center; justify-content: center; gap: 4px;
}
.talk-bar {
  flex: 0 1 4px; width: 4px; height: 100%; border-radius: var(--pill);
  background: var(--ink); opacity: 0.82;
  transform: scaleY(0.12); transform-origin: center;
}
.talk-pill.locked .talk-bar { background: var(--accent-text); opacity: 1; }

/* Last exchange */
.exchange {
  max-height: calc(var(--app-h, 100dvh) * 0.28); overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface); border-radius: var(--radius-card); padding: 12px 16px;
}
.her-line {
  margin: 0 0 8px auto; width: fit-content; max-width: 85%;
  padding: 8px 14px; border-radius: 18px; background: var(--her-bubble); color: var(--ink); /* web 0.3.0 pass 3: was var(--fill) */
}
/* Ivy's lines are rounded (CONTRACT 13). */
.ivy-spoken { margin: 0 0 6px; color: var(--muted); font: 15px/1.3 var(--font-round); }
.ivy-screen { font: 17px/1.3 var(--font-round); }
.ivy-screen > :first-child { margin-top: 0; }
.ivy-screen > :last-child { margin-bottom: 0; }
.ivy-screen p { margin: 0 0 8px; }
.ivy-screen .katex-display { margin: 8px 0; overflow-x: auto; overflow-y: hidden; }

/* File kind pick */
.kind-pick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.kind-name { flex-basis: 100%; color: var(--muted); font-size: 15px; overflow-wrap: anywhere; }

/* Chat bar: frosted glass, floating */
.chat {
  display: grid; grid-template-columns: 50px minmax(0, 1fr) auto; gap: 8px; align-items: center;
  padding: 6px; border-radius: var(--pill);
  position: relative; z-index: 2;
}
.chat .btn:not(.btn-accent) { box-shadow: none; background: var(--fill); }
.chat-input {
  min-height: 50px; width: 100%; padding: 0 18px;
  border: 0; border-radius: var(--pill);
  background: var(--fill); color: var(--ink);
  font: 17px var(--font);
}
.chat-input::placeholder, .search-input::placeholder, .rename-input::placeholder { color: var(--muted); }

/* ---------- History (CONTRACT 14) ---------- */
.history { max-width: 720px; min-height: 100vh; min-height: 100dvh; }
.history-view { display: flex; flex-direction: column; gap: 12px; padding-bottom: 24px; }
.navbar {
  position: sticky; top: max(8px, env(safe-area-inset-top)); z-index: 3;
  display: flex; align-items: center; gap: 8px;
  padding: 6px; border-radius: var(--pill); align-self: flex-start;
}
.nav-back { gap: 6px; padding: 0 16px 0 12px; color: var(--accent-text); box-shadow: none; background: transparent; border-color: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
.nav-back:hover, .nav-back:active { background: var(--fill); }
.large-title { margin: 4px 4px 0; font: 700 34px/1.15 var(--font-round); }
.search-field {
  display: flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 14px; border-radius: 12px;
  background: var(--fill); color: var(--muted);
}
.search-field svg { flex: none; }
.search-input {
  flex: 1; min-width: 0; min-height: 44px; border: 0; background: transparent; color: var(--ink);
  font: 17px var(--font); -webkit-appearance: none; appearance: none; padding: 0;
}
.search-input:focus { outline: none; }
.search-field:focus-within { outline: 3px solid var(--accent); outline-offset: 2px; }
.history-note { margin: 4px; color: var(--muted); font-size: 15px; }

/* The grouped inset list: one card, hairlines between rows, inset from the left like iOS. */
.grouped {
  list-style: none; margin: 0; padding: 0;
  background: var(--surface); border-radius: var(--radius-card); overflow: hidden;
}
.grouped > li { position: relative; }
.grouped > li + li::before {
  content: ""; position: absolute; top: 0; left: 16px; right: 0;
  border-top: var(--hair) solid var(--line);
}
.row {
  width: 100%; min-height: 44px; padding: 11px 14px 11px 16px;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 2px;
  align-items: center;
  border: 0; background: transparent; color: var(--ink); text-align: left;
  font: 17px/1.3 var(--font); cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background-color var(--dur) var(--spring);
}
.row:hover, .row:active { background: var(--fill); }
.row-title { font: 600 17px/1.3 var(--font-round); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-side { grid-row: 1 / span 3; grid-column: 2; display: flex; align-items: center; gap: 8px; color: var(--muted); }
.row-meta { font-size: 15px; color: var(--muted); }
.row-sum { font-size: 15px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-badge {
  display: inline-block; padding: 2px 9px; border-radius: var(--pill);
  background: var(--fill-strong); color: var(--ink); font-size: 13px; font-weight: 600; white-space: nowrap;
}
.row-badge.has-flags { background: var(--accent-soft); color: var(--accent-text); }
.row-chev { opacity: 0.55; }

.detail-head { display: flex; align-items: flex-start; gap: 8px; margin: 4px 4px 0; }
.detail-title { flex: 1; min-width: 0; margin: 0; font: 700 28px/1.2 var(--font-round); overflow-wrap: anywhere; }
.rename-form { display: flex; gap: 8px; align-items: center; margin: 4px 0 0; }
.rename-input {
  flex: 1; min-width: 0; min-height: 50px; padding: 0 18px;
  border: 0; border-radius: var(--pill); background: var(--surface); color: var(--ink);
  font: 600 17px var(--font-round);
}
.detail-meta { margin: 0 4px; color: var(--muted); font-size: 15px; }
.transcript .turn-item { padding: 12px 16px; }
.transcript .turn-item .her-line { margin-bottom: 8px; }
.transcript .turn-item .ivy-spoken { margin-bottom: 6px; }
.transcript .turn-item.muted { font-size: 15px; }
/* The board in History is a record, not a live step: the whole problem, every chip's value. */
.history .board-problem { display: block; -webkit-line-clamp: unset; overflow: visible; }
.history .slot { flex: 0 0 auto; min-width: 128px; max-width: 240px; }

/* ---------- three layouts ---------- */
/* iPad portrait narrow and large phones: chips keep their values and scroll sideways */
@media (max-width: 743px) {
  .slot { flex: 0 0 auto; min-width: 104px; max-width: 220px; }
}
/* Phone: the problem line and a scrollable chip row; only the lit chip shows its value */
@media (max-width: 600px) {
  .board { padding: 12px 14px; }
  .board-problem {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .board-step { margin-bottom: 8px; }
  /* 0.2.0: was ".slot:not(.lit) { display: none }"; now every chip is in the row and it scrolls (CONTRACT 13). */
  .slot {
    min-width: 0; max-width: 78%; min-height: 36px; padding: 0 14px; border-radius: var(--pill);
    flex-direction: row; align-items: center; gap: 8px;
  }
  .slot:not(.lit) .slot-value { display: none; }
  .history .slot:not(.lit) .slot-value { display: inline; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .slot.lit .slot-value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .ivy-says { font-size: 34px; }
  .session { gap: 8px; }
  .detail-title { font-size: 24px; }
}
/* Short screens, like an iPad in landscape with the keyboard up */
@media (max-height: 560px) {
  .board-step { display: none; }
  .slot { min-height: 0; }
}

/* ---------- parent page ---------- */
.parent { max-width: 900px; min-height: 100dvh; display: flex; flex-direction: column; gap: 16px; }
.parent h1 { margin: 8px 4px 0; font: 700 34px/1.15 var(--font-round); }
.parent h2 { margin: 0 4px; font: 700 22px/1.2 var(--font-round); }
.parent .bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
.parent .muted { color: var(--muted); }
.parent .soon-row { display: flex; flex-wrap: wrap; gap: 8px; }
.parent .student-head { margin: 8px 4px 0; font-size: 13px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.parent .student-group + .student-group { margin-top: 16px; }
.session-list { display: flex; flex-direction: column; gap: 8px; }
.turn { background: var(--surface); border-radius: var(--radius-card); padding: 14px 16px; }
.turn + .turn { margin-top: 10px; }
.turn .who { font-size: 13px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 8px 0 2px; }
.turn .who:first-child { margin-top: 0; }
.turn .numbers { margin-top: 10px; font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
.flag { padding: 10px 14px; border-left: 4px solid var(--accent); background: var(--surface); border-radius: 12px; margin-bottom: 6px; }

/* Reduce Motion: every transition is instant. The pill's bars are moved by script, so they still move. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}

/* ---------- waiting motion (CONTRACT 15), web 0.4.0 ----------
   While Ivy waits on something, the place it will land moves, calmly, and stops the frame it lands.
   Every cycle is 2.4 s or slower, ease in and out, opacity and transform and a sheen only, so
   nothing reflows. The marks come from web/wait.js: aria-busy and data-wait on the waiting region.
   Placeholders are the size of what they stand in for, so the page does not shift when it lands. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
@keyframes wait-breath { 0%, 100% { opacity: 0.3; transform: scale(0.8); } 50% { opacity: 0.9; transform: scale(1); } }
@keyframes wait-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@keyframes wait-fade-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.78; } }
@keyframes wait-glow { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
@keyframes wait-orb { 0%, 100% { transform: scale(1); } 50% { transform: scale(var(--wait-swell)); } }
@keyframes wait-sheen { from { background-position: 100% 0; } to { background-position: 0% 0; } }

/* A dot where the words that are coming will land: Ivy's line, her bubble, a file on its way. */
.wait-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-inline-start: 0.35em; vertical-align: middle;
  background: currentColor;
  animation: wait-breath var(--wait-breath) var(--wait-ease) infinite;
}
.her-line.pending .wait-dot:first-child, .ivy-spoken.pending .wait-dot:first-child { margin-inline-start: 0; }

/* A placeholder line, one line tall, with a slow sheen passing over it. */
.wait-bar { display: block; position: relative; color: transparent; overflow: hidden; }
.wait-bar::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0.2em; bottom: 0.2em; border-radius: var(--pill);
  background: linear-gradient(90deg, var(--fill) 0%, var(--fill) 35%, var(--wait-sheen) 50%, var(--fill) 65%, var(--fill) 100%);
  background-size: 300% 100%;
  animation: wait-sheen var(--wait-sheen-dur) var(--wait-ease) infinite;
}
.ivy-screen .wait-bar-screen { width: 64%; margin: 0; }
.row-title.wait-bar { width: 58%; }
.row-meta.wait-bar { width: 34%; }
.row-sum.wait-bar { width: 82%; }
.skeleton-item .row { cursor: default; }
.skeleton-item .row:hover { background: transparent; }
.transcript .wait-bar-her { width: 46%; margin: 0 0 8px auto; padding: 8px 0; font-size: 17px; line-height: 1.3; }
.transcript .wait-bar-spoken { width: 78%; margin: 0; font: 15px/1.3 var(--font-round); }
.board-problem .wait-bar { width: 70%; }

/* The start orb breathes while the page boots and while a session closes. */
html:not([data-ready]) .start-orb, .start[data-wait] .start-orb {
  animation: wait-orb var(--wait-breath) var(--wait-ease) infinite;
}
/* The hint under the orb breathes "Getting the mic ready" until the mic answers. */
.session[data-wait~="mic"] .orb-hint { animation: wait-fade var(--wait-breath) var(--wait-ease) infinite; }
/* A busy button, link or row: a soft accent glow breathes over it until its answer comes. */
.btn[aria-busy="true"], .link[aria-busy="true"], .row[aria-busy="true"] { position: relative; }
.btn[aria-busy="true"]::after, .link[aria-busy="true"]::after, .row[aria-busy="true"]::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: var(--wait-tint);
  animation: wait-glow var(--wait-breath) var(--wait-ease) infinite;
}
.link[aria-busy="true"]::after { border-radius: var(--radius-chip); }

/* Reduce Motion: nothing travels, nothing swells. Each wait keeps a gentle opacity pulse, so the
   page is never still while Ivy works. These beat the block above by weight, and say so. */
@media (prefers-reduced-motion: reduce) {
  html .wait-dot { animation: wait-fade var(--wait-breath) var(--wait-ease) infinite !important; transform: none; }
  html .wait-bar::before { background: var(--fill); animation: wait-fade var(--wait-breath) var(--wait-ease) infinite !important; }
  html:not([data-ready]) .start-orb, html .start[data-wait] .start-orb { animation: wait-fade-soft var(--wait-breath) var(--wait-ease) infinite !important; }
  html .session[data-wait~="mic"] .orb-hint { animation: wait-fade var(--wait-breath) var(--wait-ease) infinite !important; }
  html .btn[aria-busy="true"]::after, html .link[aria-busy="true"]::after, html .row[aria-busy="true"]::after {
    animation: wait-glow var(--wait-breath) var(--wait-ease) infinite !important;
  }
}

/* ---------- waiting motion, pass 2 of the "keep it moving" loop (web 0.4.1) ----------
   The fixes from the pass 1 verdict. The rules above stay as they were; these come after them and
   win by order, and each says what it replaces.
   1. Calm: the dot breathes in opacity only. Pass 1 swelled it from 0.8 to 1, a 25% swell where the
      ceiling is the orb's 0.022. Nothing waiting now swells past 0.022; only the start orb swells,
      at exactly that. */
@keyframes wait-breath { 0%, 100% { opacity: var(--wait-dim); } 50% { opacity: var(--wait-lit); } }
@keyframes wait-fade { 0%, 100% { opacity: 1; } 50% { opacity: var(--wait-fade-low); } }
@keyframes wait-fade-soft { 0%, 100% { opacity: 1; } 50% { opacity: var(--wait-fade-soft-low); } }
/* 2. The dot after Ivy's words takes no room on its line (its width is given back by a negative
      margin), so it can come and go without rewrapping the words it follows. */
.ivy-spoken .wait-dot { margin-inline-end: calc(-8px - 0.35em); }
.ivy-spoken.pending .wait-dot:first-child { margin-inline-end: -8px; }
/* 3. The exchange card takes the room an answer can take once she starts an exchange, and keeps it
      until the session ends (app.js openRoom). Same value as its max-height above. */
.exchange.room { min-height: var(--wait-room); }
/* 4. The hint spans the orb's column, so a new hint of another width does not move its box. It reads
      the same: the text was centered in it already. */
.orb-wrap > .orb-hint { align-self: stretch; }
/* 5. The sheen runs on the compositor: a light band moved by transform inside the placeholder, clipped
      to its pill, over a still fill. Pass 1 moved a background-position, which repaints each frame. */
@keyframes wait-sheen { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
.wait-bar { clip-path: inset(0.2em 0 round var(--pill)); }
.wait-bar::before { background: var(--fill); animation: none; }
.wait-bar::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 40%; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--wait-sheen), transparent);
  animation: wait-sheen var(--wait-sheen-dur) var(--wait-ease) infinite;
}
/* 6. A busy control breathes a ring at its edge and a soft glow inside it, and keeps its own face.
      Pass 1 washed the whole face in the tint, which read as disabled. */
.btn[aria-busy="true"]::after, .link[aria-busy="true"]::after, .row[aria-busy="true"]::after {
  background: none;
  box-shadow: inset 0 0 0 1.5px var(--wait-edge), inset 0 0 14px var(--wait-tint);
}
/* 7. The start screen boots in the signed-in layout: History and Sign out keep their room, unseen,
      until sign-in is known, so a known sign-in lands with nothing moving. */
html:not([data-ready]) .start-foot > :is(#history-btn, #signout),
.start[data-wait~="boot"] .start-foot > :is(#history-btn, #signout) { visibility: hidden; }
/* 8. A session's board while it loads: three problem lines under 600 px and two above, the size a
      problem takes; a placeholder step line; the chips dimmed, with no step lit and no values, so
      nothing claims a step before the data does. The whole board is rebuilt when the session lands. */
.board.loading .slot, .board.loading .slot.lit { background: var(--fill); color: var(--muted); }
.board.loading .slot .slot-label { color: var(--muted); }
.board.loading .slot.done .slot-label::after { content: none; }
.history .board.loading .slot .slot-value, .board.loading .slot .slot-value { display: none; }
.board.loading .board-problem .wait-bar { width: 100%; }
.board.loading .board-problem .wait-bar:nth-child(2) { width: 92%; }
.board.loading .board-problem .wait-bar:nth-child(3) { width: 56%; }
.board.loading .board-step .wait-bar { width: 22%; }
@media (min-width: 601px) { .board.loading .board-problem .wait-bar:nth-child(3) { display: none; } }
/* 9. The parent page's placeholders: a student heading over three rows, as the real list draws it,
      and one turn card while a session opens. */
.parent[data-boot] #signout { visibility: hidden; }
.student-head.wait-bar { width: 18%; }
.turn.skeleton-item .numbers + .numbers { margin-top: 0; }
.turn.skeleton-item .who.wait-bar { width: 14%; }
.turn.skeleton-item .line.wait-bar { width: 88%; }
.turn.skeleton-item .line.short.wait-bar { width: 52%; }
.turn.skeleton-item .numbers.wait-bar { width: 70%; }

@media (prefers-reduced-motion: reduce) {
  html .wait-bar::after { display: none; }
  html .wait-bar::before { animation: wait-fade var(--wait-breath) var(--wait-ease) infinite !important; }
}

/* ---------- waiting motion, pass 3 of the "keep it moving" loop (web 0.4.2) ----------
   The fixes from the pass 2 verdict. The rules above stay as they were; these come after them and
   win by order, and each says what it replaces.
   1. The exchange card's room is a track of the session grid, open from the moment the session
      screen shows (app.js showScreen), so the orb is drawn at its session size and nothing moves at
      her first send. The card sits at the top of its room at its own height, under the orb, and
      scrolls inside it past the room. Pass 2 rule 3 stretched the card itself to the room from her
      first tap, which shrank the orb under her finger and left a tall empty white card while she was
      heard. The row order is the session's own: top bar, board, orb, exchange, then the rest. */
.session.room { grid-template-rows: auto auto minmax(0, 1fr) var(--wait-room) auto auto; }
.session.room > .exchange { align-self: start; max-height: 100%; }
/* 2. The dot after her words ("Sending page.png") takes the same gap as the dot after Ivy's. */
.her-line.pending .wait-dot.after-text { margin-inline-start: var(--wait-dot-gap); }
/* 3. The dot's size, its gap and the room it gives back are tokens. Were 8px, 0.35em, calc(-8px -
      0.35em) and -8px. */
.wait-dot { width: var(--wait-dot); height: var(--wait-dot); margin-inline-start: var(--wait-dot-gap); }
.her-line.pending .wait-dot:first-child:not(.after-text), .ivy-spoken.pending .wait-dot:first-child { margin-inline-start: 0; }
.ivy-spoken .wait-dot { margin-inline-end: calc(-1 * (var(--wait-dot) + var(--wait-dot-gap))); }
.ivy-spoken.pending .wait-dot:first-child { margin-inline-end: calc(-1 * var(--wait-dot)); }
/* 4. The busy ring and glow take their width and blur from tokens. Were 1.5px and 14px. */
.btn[aria-busy="true"]::after, .link[aria-busy="true"]::after, .row[aria-busy="true"]::after {
  box-shadow: inset 0 0 0 var(--wait-ring) var(--wait-edge), inset 0 0 var(--wait-glow-blur) var(--wait-tint);
}
/* 5. The board's problem line keeps two lines of room in a session, open with the screen like the
      card's. A fresh session reads "No problem yet. Show me one." on one line, and the problem that
      lands after her first exchange took two on a phone, which shrank the orb (237 to 215 px at
      390). Two lines is the phone's clamp, and a phone's chips show no values but the lit one's, so
      the phone's board never moves. Above 600 px it is left as it was: a short problem fits one line
      there, and the chips grow as their values land, so a room would only cost the orb a line.
      History's board is left as it was. */
@media (max-width: 600px) { .session.room .board-problem { min-height: var(--wait-problem-room); } }
