/*
 * ikutan design tokens and base styles.
 *
 * Dark-first by design: auditoriums are dark, and a full-brightness white phone
 * screen in a dark hall is rude to the people behind you. Light theme is an
 * explicit opt-in, remembered per device.
 *
 * Every contrast ratio in DESIGN.md was measured against these values.
 */

:root {
  /* Dark is the default, not a media query. */
  --bg: #0b0b0c;
  --surface: #16161a;
  --surface-2: #1c1c1f;
  --border: #2a2a2e;
  --border-strong: #3a3a40;
  --text: #f4f4f5;
  --text-muted: #a1a1a8;
  --text-faint: #80808a;
  --accent: #4ade80;
  --accent-ink: #04140a;
  --accent-dim: #1e5f38;
  --warn: #fbbf24;
  --danger: #f87171;
  /* Set against the danger fill. The fill is light in this theme, so the ink is
     dark; the light theme is the inverse. */
  --danger-ink: #2b0b0b;
  --scrim: rgba(11, 11, 12, 0.72);
  /*
   * Tinted shadows.
   *
   * A pure-black shadow at low opacity reads as grey haze. These carry the hue of
   * the surface underneath, which is what makes elevation look like light rather
   * than dirt. Values stay under 0.5 opacity because the skill's rule is that
   * shadows should be barely there on a dark canvas.
   */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow-md: 0 4px 16px -4px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 16px 48px -12px rgba(0, 0, 0, 0.5);
  /* Selection and focus use the accent so the page reads as one system. */
  --ring: 0 0 0 3px rgba(74, 222, 128, 0.18);
  --focus: #4ade80;

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-full: 999px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;

  /*
   * Spring-like easing.
   *
   * `cubic-bezier(0.16, 1, 0.3, 1)` overshoots slightly and settles, which reads
   * as weight. Linear and ease-in-out read as mechanical. Both design skills call
   * out default easing specifically.
   */
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 120ms;
  --dur-base: 220ms;
  --dur-slow: 420ms;
  --dur-enter: 600ms;

  /*
   * Geist first, with a full system fallback.
   *
   * The fallback chain is not decoration: the audience viewer never loads the
   * webfont, so it resolves straight through to the system stack. Both are
   * present here so one variable works on every surface.
   */
  --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Mono",
    Menlo, Consolas, "Liberation Mono", monospace;

  --z-deck: 1;
  --z-controls: 10;
  --z-pill: 20;
  --z-overlay: 30;
  --z-toast: 40;
}

/*
 * Light theme, opt-in.
 *
 * The deck surface stays dark even here: a white slide at full brightness is
 * exactly what this product exists to avoid. Only the app chrome flips.
 */
:root[data-theme="light"] {
  --bg: #fafafa;
  --surface: #ffffff;
  --surface-2: #f4f4f5;
  --border: #e4e4e7;
  --border-strong: #d4d4d8;
  --text: #18181b;
  --text-muted: #52525b;
  --text-faint: #71717a;
  --accent: #15803d;
  --accent-ink: #ffffff;
  --accent-dim: #bbf7d0;
  --warn: #a16207;
  --danger: #b91c1c;
  --danger-ink: #ffffff;
  --scrim: rgba(250, 250, 250, 0.82);
  --focus: #15803d;
}

@media (prefers-contrast: more) {
  :root {
    --border: var(--border-strong);
    --text-muted: var(--text);
    --text-faint: var(--text-muted);
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Prevent iOS text inflation on landscape rotation. */
  -webkit-text-size-adjust: 100%;
}

/* Visible focus everywhere. This is a keyboard-first tool. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

:focus:not(:focus-visible) {
  outline: none;
}

/* ---------------------------------------------------------------- typography */

/*
 * Type scale.
 *
 * The previous h1 topped out at 32px, which is body-text scale: a page whose
 * only job is to send a speaker to one action had no visual anchor. The display
 * size is roughly double, with tracking tightened as size grows, which is what
 * keeps a large heading from looking loose.
 *
 * Every step is set in rem-relative sizes with a `clamp` where width matters, so
 * a 390px phone and a 1440px laptop both get a deliberate size rather than a
 * scaled-down desktop.
 */
h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  line-height: 1.15;
  /* Avoids a single orphaned word on the last line of a heading. */
  text-wrap: balance;
}

h1 {
  font-size: clamp(30px, 4.2vw, 44px);
  letter-spacing: -0.035em;
  line-height: 1.05;
  font-weight: 620;
}

h2 {
  font-size: 17px;
  letter-spacing: -0.015em;
}

h3 {
  font-size: 15px;
  letter-spacing: -0.01em;
}

p {
  margin: 0;
  /* Roughly 65 characters, the range both design skills name. */
  max-width: 68ch;
  text-wrap: pretty;
}

/*
 * Numbers line up.
 *
 * Slide counts, elapsed time and room codes are all read in columns or against
 * each other, and a proportional font makes them jitter as they change.
 */
.mono,
.stat-value,
.counter,
.deck-code,
.join-code-big {
  font-variant-numeric: tabular-nums;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

.mono {
  font-family: var(--font-mono);
  font-variant-ligatures: none;
}

/* ------------------------------------------------------------------ layout */

.shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* The main region grows, pushing a trailing band to the foot of the viewport. */
.shell > main {
  flex: 1;
}

.wrap {
  width: 100%;
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.wrap-narrow {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.spacer {
  flex: 1;
}

/* ------------------------------------------------------------------- chrome */

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 56px;
  padding: 0 var(--space-4);
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: var(--z-controls);
  /* Wraps instead of overflowing. On a 390px phone the wordmark, title, two
     buttons and the join link do not fit on one line, and the join button was
     pushing 10px past the viewport, which shifts the whole page sideways. */
  flex-wrap: wrap;
}

@media (max-width: 560px) {
  .topbar {
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    flex-wrap: nowrap;
  }

  .account-email {
    display: none;
  }
}

/* ----------------------------------------------------------------- wordmark */

/*
 * Space Grotesk, all caps, wide tracking.
 *
 * The centre M takes the accent. It sits on the palindrome's axis, so the
 * colour marks the same point the symmetry does.
 */
.wm {
  font-family: "Space Grotesk", var(--font-sans);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.17em;
  /*
   * The tracking after the final T is real but invisible, and it pushes the
   * mark off centre against anything beside it. Pulling it back is the
   * difference between aligned and almost-aligned.
   */
  margin-right: -0.17em;
  line-height: 1;
  color: var(--text);
  white-space: nowrap;
}

.wm-m {
  color: var(--accent);
}

/*
 * The lockup: the wordmark over its own reflection.
 *
 * Display sizes only — see the note in wordmark.tsx. Space Grotesk's M is wide
 * and flat, which is what makes the mirror read.
 */
.wm-lockup {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
}

.wm-mirror {
  display: block;
  transform: scaleY(-1);
  opacity: 0.22;
  /* Tucked up under the word so the two read as one object, not two lines. */
  margin-top: -0.10em;
  -webkit-mask-image: linear-gradient(180deg, transparent 32%, #000 100%);
  mask-image: linear-gradient(180deg, transparent 32%, #000 100%);
}

/*
 * Below ~40px the mirrored letterforms stop resolving and the reflection turns
 * into a smudge, so it is dropped and the plain wordmark stands alone.
 */
@media (max-width: 860px) {
  .wm-mirror {
    display: none;
  }
}

.topbar-title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* ------------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 550;
  letter-spacing: -0.005em;
  padding: 0 var(--space-4);
  height: 40px;
  border-radius: var(--r-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform 90ms var(--ease);
  text-decoration: none;
}

.btn:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.btn:active {
  transform: scale(0.98);
}

/*
 * Disabled.
 *
 * Opacity alone left the primary button at a muddy mid-green that read as a
 * second, unintentional accent on a page whose rule is one accent colour. A
 * neutral surface plus reduced contrast says "not yet" without inventing a shade.
 */
.btn[disabled],
.btn[aria-disabled="true"] {
  cursor: not-allowed;
  transform: none;
  opacity: 1;
}

/*
 * Clearly not ready.
 *
 * A grey fill alone still reads as a button that might work. A dashed border and
 * a muted label say "not yet" without inventing a shade, and the cursor change
 * carries the same meaning for a mouse user.
 */
.btn-primary[disabled] {
  background: transparent;
  border-color: var(--border);
  border-style: dashed;
  color: var(--text-faint);
}

.btn:not(.btn-primary)[disabled] {
  opacity: 0.55;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 650;
}

.btn-primary:hover {
  background: var(--accent);
  border-color: var(--accent);
  filter: brightness(1.08);
}

/*
 * A quiet button that states its consequence in colour.
 *
 * Delete previously carried its colour as an inline style, which meant the one
 * place in the app that sets a destructive affordance was also the one place that
 * bypassed the stylesheet.
 */
/*
 * The destructive action in a confirmation.
 *
 * Filled rather than outlined, because it is the one thing the dialog exists to
 * ask about, and unlike the quiet Delete in a list it should not be easy to miss.
 * Ink is set explicitly against the fill so it passes contrast in both themes.
 */
.btn-danger-solid {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--danger-ink);
  font-weight: 650;
}

.btn-danger-solid:hover {
  /* Restated, not inherited. `.btn:hover` is the same specificity and sets a
     light surface fill. Without these, the label stays white and disappears. */
  background: var(--danger);
  border-color: var(--danger);
  color: var(--danger-ink);
  filter: brightness(1.06);
}

.btn-danger {
  color: var(--danger);
}

.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
}

.btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
}

.btn-lg {
  height: 48px;
  font-size: 15px;
  padding: 0 var(--space-6);
}

/* For the cluster of actions beside a password field, where four full-size
   buttons would wrap and read as four equally weighted choices. */
.btn-sm {
  height: 34px;
  font-size: 13px;
  padding: 0 var(--space-3);
}

/* 44px minimum touch target on anything interactive. */
.btn-icon {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--r-md);
}

.theme-toggle {
  flex: none;
}

.theme-toggle [hidden] {
  display: none;
}

/* -------------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--space-4);
}

.card-lg {
  border-radius: var(--r-lg);
  padding: var(--space-6);
}

/* ------------------------------------------------------------------- inputs */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.label {
  font-size: 13px;
  font-weight: 550;
  color: var(--text-muted);
}

/*
 * The label above a value someone reads out or copies: a room code, a position, a
 * join link.
 *
 * Lives here rather than in one page's stylesheet because the same datum appears
 * on more than one surface. The home page labelled the room code "ROOM CODE" while
 * the session panel labelled the identical code "Join code" in a different face,
 * which made two screens describing one thing look like two products.
 */
.meta-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.input {
  font: inherit;
  font-size: 16px; /* 16px prevents iOS zoom on focus. Not a style choice. */
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  height: 44px;
  padding: 0 var(--space-3);
  width: 100%;
}

.input::placeholder {
  color: var(--text-faint);
}

/*
 * A text field states focus once, on its border.
 *
 * The global `:focus-visible` outline is drawn on top of the border change below,
 * so a focused field carried two green rings: the border it already changes, and
 * an outline floating two pixels outside it. Two indicators for one state read as
 * a rendering fault rather than emphasis.
 *
 * Suppressed only here, and only because the border change is a real indicator:
 * it moves the field from the resting hairline to the accent, which is a large
 * enough shift in both themes to be unmistakable. Buttons keep the outline,
 * because they have no focus style of their own and a keyboard needs to find them.
 */
.input:focus-visible {
  outline: none;
}

.input:focus {
  border-color: var(--accent);
}

.input-code {
  font-family: var(--font-mono);
  font-size: 24px;
  letter-spacing: 0.18em;
  text-align: center;
  text-transform: uppercase;
  height: 56px;
  font-weight: 600;
}

.hint {
  font-size: 13px;
  color: var(--text-muted);
}

.error {
  font-size: 13px;
  color: var(--danger);
  font-weight: 550;
}

/* ------------------------------------------------------------------- badges */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 10px;
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  white-space: nowrap;
}

.badge-live {
  color: var(--accent);
  border-color: var(--accent-dim);
}

/*
 * Connected, but the talk has not started.
 *
 * Deliberately not green: the panel next to this says "Ready when you are", and a
 * green badge reading "live" contradicted it. Neutral, and no pulsing dot — the
 * pulse means something is happening, and nothing is yet.
 */
.badge-ready {
  color: var(--text-muted);
}

.badge-warn {
  color: var(--warn);
}

.badge-danger {
  color: var(--danger);
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: currentColor;
  flex: none;
}

/* A live dot is the one piece of decoration in the app, and it carries state. */
.badge-live .dot {
  animation: pulse 2s var(--ease) infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* -------------------------------------------------------------------- lists */

.list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface);
}

.list-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
  min-height: 56px;
}

.list-item:last-child {
  border-bottom: none;
}

a.list-item:hover {
  background: var(--surface-2);
}

/* ------------------------------------------------------------- empty states */

.empty {
  border: 1px dashed var(--border);
  border-radius: var(--r-md);
  padding: var(--space-8) var(--space-4);
  text-align: center;
  color: var(--text-muted);
}

/* --------------------------------------------------------------- utilities */

.muted {
  color: var(--text-muted);
}

.faint {
  color: var(--text-faint);
}

.small {
  font-size: 13px;
}

.tiny {
  font-size: 12px;
}

.center {
  text-align: center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/*
 * The `hidden` attribute is authoritative.
 *
 * A class rule with `display: grid` or `display: inline-flex` beats the browser
 * default of `[hidden] { display: none }`, so an element with the attribute set
 * stays visible. Making this explicit means JS only ever toggles `hidden`.
 */
[hidden] {
  display: none !important;
}

/* --------------------------------------------------------------- join card */

/*
 * The join affordance.
 *
 * Visually distinct from the speaker's own content because it serves a different
 * person: an audience member who was handed a code and needs to know exactly
 * where to type it.
 */
.join-card {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: var(--space-5);
}

.join-card-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

@media (min-width: 640px) {
  .join-card-form {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
  }
}


/* -------------------------------------------------------------- deck list */

/*
 * One row per deck, with its live state attached.
 *
 * Rows rather than cards: a speaker scans a list for the thing that is still
 * running, and cards add vertical weight that makes scanning slower. The live
 * row is the only one with a tinted surface, so the eye lands on it first.
 */
.deck-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.deck-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  flex-wrap: wrap;
}

.deck-row-main {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  flex: 1 1 260px;
}

.deck-row-title {
  font-size: 16px;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.deck-row-title:hover {
  color: var(--accent);
}

.deck-row-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  margin-left: auto;
}

/* The one row that needs to draw the eye: a talk is still running. */
.deck-row-live {
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}

.deck-row-muted {
  background: transparent;
  border-style: dashed;
}

/* ------------------------------------------------------------------ pills */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  color: var(--text-muted);
  flex: none;
}

.pill-live {
  color: var(--accent);
  border-color: var(--accent-dim);
}

.pill-live .dot {
  background: currentColor;
  animation: pulse 2s var(--ease) infinite;
}

/* The room code is read aloud and typed by hand, so it is mono and wide. */
.deck-code {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--text);
}

@media (prefers-reduced-motion: reduce) {
  .pill-live .dot {
    animation: none;
  }
}


/* ---------------------------------------------------------- verify fixes */

/*
 * Small tap-target corrections found by the responsive sweep.
 *
 * The wordmark is a navigation link, so it needs a real touch target rather than
 * the 21px its text happens to occupy. Padding does it without changing the
 * visual position.
 */
.topbar-title {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-right: var(--space-2);
}

/*
 * Up one level, next to the wordmark.
 *
 * Left, because that is where a reader looks for it and because the right-hand
 * cluster is the far corner — the hardest place to reach on a phone held in one
 * hand, and where it shared a group with Reports, Sign out and the theme toggle.
 *
 * 44px tall rather than the 34px a small button would give: it is navigation, and
 * on touch it is the control most likely to be pressed one-handed while walking.
 */
.topbar-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
  padding: 0 var(--space-3) 0 var(--space-2);
  margin-left: calc(var(--space-2) * -1);
  border-radius: var(--r-md);
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 550;
  letter-spacing: -0.005em;
  text-decoration: none;
  white-space: nowrap;
}

.topbar-back:hover {
  color: var(--text);
  background: var(--surface);
}

.topbar-back:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/*
 * The label goes on a phone, the chevron stays.
 *
 * Six controls in a bar that fits five is what pushed "Sign out" off the edge of
 * a 390px screen. A bare chevron in the top-left is the one navigation mark that
 * needs no label, and it keeps the target exactly where the thumb expects it.
 */
/*
 * On a phone, the corner belongs to navigation.
 *
 * Measured rather than guessed. At 360px the bar has 336px to work with, and the
 * wordmark, Reports, the account control and the theme toggle already spend 344.
 * A labelled way up needs about 80 more, and finding it by dropping Reports would
 * put the admin page out of reach on a phone. So on a phone the wordmark yields —
 * it is on the landing page, the sign-in page and the library, which is where a
 * brand belongs; on a task page the top-left corner is where a reader looks for a
 * way back, and that is the control worth spending the space on.
 *
 * The brand returns above 560px, where there is room for both.
 */
@media (max-width: 560px) {
  .topbar-has-back .topbar-title {
    display: none;
  }

  .topbar-back {
    margin-left: 0;
    padding: 0 var(--space-3) 0 var(--space-2);
    color: var(--text);
  }
}

/* The cockpit's way out shares the header with stats, so it is sized but quiet. */
.btn-back {
  gap: var(--space-1);
  min-height: 44px;
  padding: 0 var(--space-3);
}

/* The deck title is the primary row action on touch, so it needs 44px too. */
.deck-row-title {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}


/*
 * Destructive action.
 *
 * Muted until hovered: it should be findable when looked for and easy to miss
 * when reaching for Present. Colour appears on interaction so the row does not
 * read as a warning.
 */
.btn-icon-danger {
  color: var(--text-faint);
  width: 40px;
  height: 40px;
  padding: 0;
}

.btn-icon-danger:hover {
  color: var(--danger);
  background: var(--surface-2);
}

/* --------------------------------------------------------------- entrance */

/*
 * Staggered entry.
 *
 * The skill's rule is explicit: never mount everything at once. Items fade and
 * rise a short distance, cascading by index. `--index` is set inline by the view,
 * so a list of any length cascades without a class per item.
 *
 * Transform and opacity only, so this never triggers layout.
 */
@keyframes enter {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.enter {
  animation: enter var(--dur-enter) var(--ease-out-expo) both;
  animation-delay: calc(var(--index, 0) * 60ms);
}

/*
 * Reduce motion collapses the cascade to a plain appearance.
 *
 * Nothing may be left invisible: the animation is `both` fill mode, so canceling
 * the keyframes without resetting opacity would hide the element entirely.
 */
@media (prefers-reduced-motion: reduce) {
  .enter {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------- texture */

/*
 * A very faint grain over the whole app.
 *
 * Flat vector surfaces read as sterile, and this is the cheapest way to give a
 * dark UI the sense of a physical material. It sits on a fixed, pointer-events
 * none layer so it never intercepts a click and never repaints on scroll, which
 * is the only safe way to use a filter like this on mobile.
 */
.grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  pointer-events: none;
  opacity: 0.022;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-transparency: reduce) {
  .grain {
    display: none;
  }
}

/* ------------------------------------------------------------ room password */

/* Sparse by design: one field, one action, nothing to wander off to. */
.unlock-main {
  max-width: 440px;
  padding-top: clamp(48px, 12vh, 120px);
  padding-bottom: var(--space-12);
}

/* The room code is a small reassurance that the link resolved somewhere real. */
.unlock-code {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--text-faint);
}

/* ------------------------------------------------------------- session panel */

/*
 * The room, prepared before the talk.
 *
 * This screen replaces a Present button and a password field hidden inside the QR
 * overlay. It is the one place a speaker sets up a session, so it is a single
 * panel: a header stating the room's condition, the joining details, the access
 * control, and one primary action at the foot.
 *
 * The rows stack rather than sitting side by side. In two columns the join code and
 * the password field fought over the same width and the link was clipped to
 * "https://ikutan.app.benwara.c...", which is unreadable at the moment someone is
 * checking they have the right address.
 */
.session-panel {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
}

.session-panel-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
  color: var(--text-faint);
}

.session-panel-head strong {
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
}

/* Each row is a band of the panel, divided so the steps read in order. */
.session-row {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-5);
  border-bottom: 1px solid var(--border);
}

/*
 * The QR sits on a white plate because an inverted or tinted code is a code that
 * does not scan, and the dark theme would otherwise leave it on a dark ground.
 */
.session-qr-wrap {
  flex: 0 0 auto;
  background: #ffffff;
  border-radius: var(--r-md);
  padding: var(--space-2);
  display: grid;
  place-items: center;
}

.session-qr-wrap .join-qr {
  display: block;
  width: 148px;
  height: 148px;
}

.session-join > .session-field {
  flex: 1;
}

.session-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.session-link {
  align-items: stretch;
}

.session-link .session-field {
  flex: 1;
}

.session-code {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: 0.16em;
  line-height: 1.1;
  /* The code must never spill sideways into anything beside it. */
  overflow-wrap: anywhere;
}

.copy-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/*
 * The link wraps rather than truncates.
 *
 * An ellipsis here hides the one part a person is checking: the code at the end.
 * Wrapping keeps the whole address legible, and the URL is the row's own content
 * so it can take the width it needs.
 */
.copy-value {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------ access row */

/*
 * A plain row rather than a boxed-off aside: this is the control two speakers went
 * looking for and could not find, so it reads as part of the panel's main flow
 * instead of something tucked away.
 */
.session-lock {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
}

/*
 * The state is a sentence, not a label.
 *
 * It was set in the uppercase micro style, which this app uses for labelling a
 * value rather than reporting one, and uppercase flattened the two states into
 * near-identical blocks of text.
 */
.lock-state {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 14px;
  color: var(--text-muted);
}

/* Only the locked state carries ink, so the difference is visible peripherally. */
.lock-state-on {
  color: var(--text);
}

.lock-state-on svg {
  color: var(--accent);
}

/*
 * The field and the buttons beside it share one height.
 *
 * They did not: the field was 44px for a comfortable touch target, the buttons
 * were 34px because they were small, and 10px of mismatch in a single row reads as
 * a mistake no matter how each control looks on its own. Declared once here so the
 * two cannot drift apart again.
 */
.session-lock .lock-form {
  --control-h: 40px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.session-lock .lock-form .input,
.session-lock .lock-form .btn {
  height: var(--control-h);
  border-radius: var(--r-md);
}

/* The field and its Show toggle are one control, so they are one box. */
.lock-input {
  position: relative;
  flex: 1 1 220px;
  min-width: 0;
  display: flex;
  align-items: center;
}

.lock-input .input {
  width: 100%;
  /* Room for the affixed toggle so a long password never runs underneath it. */
  padding-right: 72px;
}

/*
 * The Show toggle sits inside the field's bounds.
 *
 * Sized against the row height rather than given a height of its own, so it stays
 * centred whatever the control height is set to.
 */
.lock-affix {
  position: absolute;
  right: 5px;
  height: calc(var(--control-h) - 10px);
  padding: 0 var(--space-3);
  border-color: transparent;
  background: transparent;
  color: var(--text-muted);
}

.lock-affix:hover {
  background: var(--surface-2);
  color: var(--text);
}

/*
 * The stored password, shown in full.
 *
 * Set apart from the field below it: that field takes a new password, this states
 * the current one. A quiet surface keeps the two from reading as the same control.
 */
.lock-recall {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

/* The password itself, in the reading face and letter-spaced like the room code,
   because it is read aloud the same way. */
.lock-recall .copy-value {
  font-size: 15px;
  letter-spacing: 0.06em;
  color: var(--text);
}

.lock-note-quiet {
  color: var(--text-faint);
}

.lock-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.session-panel-foot {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  padding: var(--space-5);
}

.session-panel-foot .btn,
.session-panel-foot form .btn {
  width: 100%;
}

.session-foot-note {
  margin: 0;
}

/* ------------------------------------------------------------- deck notes */

/*
 * Notes about the deck, not alerts from the system.
 *
 * A single amber icon and a sentence. No card, no badge, no accent bar: the
 * bordered card below means "the session panel", and borrowing that treatment for
 * a one-line remark made the page read as a stack of boxes.
 */
.deck-notes {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.deck-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

/* Nudged onto the first line's optical centre rather than its box top. */
.deck-note svg {
  flex: none;
  margin-top: 2px;
  color: var(--warn);
}

/* ----------------------------------------------------------- deck actions */

.deck-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* A rule rather than a spacer: the gap is deliberate, not incidental. */
.deck-actions-sep {
  flex: 1;
  height: 1px;
  min-width: var(--space-6);
  background: var(--border);
}

/* On a phone the QR would leave the details too narrow, so the row stacks. */
@media (max-width: 560px) {
  .session-join {
    flex-direction: column;
    align-items: stretch;
  }

  .session-qr-wrap {
    align-self: center;
  }

  .session-code {
    font-size: 30px;
  }

  .session-panel-foot {
    flex-direction: column;
    align-items: stretch;
    text-align: center;
  }

  .session-panel-foot form .btn {
    width: 100%;
  }

  /*
   * The rule that separates Delete from the other deck actions is a wide-screen
   * device. Wrapped onto its own line it left a dash floating beside Review
   * slides, attached to nothing.
   */
  .deck-actions-sep {
    display: none;
  }

  .deck-actions {
    gap: var(--space-3);
  }
}

/* ------------------------------------------------------------------- modal */

/*
 * A confirmation, as a real dialog.
 *
 * Built on `<dialog>` and `showModal`, which supply focus trapping, Escape and a
 * backdrop. The alternative used here previously was `window.confirm`, which is
 * unstyleable, blocks the whole page, and looks like a browser error rather than
 * part of the product.
 */
.modal {
  /* The UA default is a bordered box glued to the top of the viewport. */
  margin: auto;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--text);
  width: min(440px, calc(100vw - 2 * var(--space-4)));
  box-shadow: 0 24px 60px -20px rgb(0 0 0 / 0.7);
}

.modal::backdrop {
  /* Blurred rather than merely dimmed: the cockpit is a live projection surface,
     and a soft backdrop keeps it legible while making clear it is out of reach. */
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(3px);
}

.modal-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  margin: 0;
}

/*
 * Every confirmation opens with the same signal: a tinted chip carrying a warning
 * icon, beside the question and its consequence.
 *
 * Both asks on this product are destructive and irreversible for other people, so
 * they should be recognisable at a glance as the same kind of moment. The tint is
 * the danger colour at low strength, which reads as a category rather than an
 * alarm, and the chip is identical in every dialog so the shape, not the words,
 * carries the meaning.
 */
.modal-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.modal-icon {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--danger) 16%, transparent);
  color: var(--danger);
}

.modal-head-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.modal-title {
  font-size: 19px;
  font-weight: 620;
  letter-spacing: -0.015em;
  line-height: 1.25;
}

.modal-text {
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-muted);
}

/*
 * The safe action sits on the left and takes focus. A dialog that opens with the
 * destructive button focused ends a live talk on a stray Enter.
 */
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

@media (max-width: 480px) {
  .modal-actions {
    flex-direction: column-reverse;
  }

  .modal-actions .btn {
    width: 100%;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .modal[open] {
    animation: modal-in 140ms ease-out;
  }

  .modal[open]::backdrop {
    animation: modal-backdrop-in 140ms ease-out;
  }
}

@keyframes modal-in {
  from {
    opacity: 0;
    transform: translateY(6px) scale(0.985);
  }
}

@keyframes modal-backdrop-in {
  from {
    opacity: 0;
  }
}


/* The signed-in control: the address, then a way out. Truncate long addresses
   rather than wrapping the header. */
.account {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.account-email {
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * "Or enter the code from the email", on the page that says we sent a link.
 *
 * A rule above it, because it is a second way to do the same thing rather than a
 * step after the first: without the separation it reads as "now do this too".
 */
.code-form {
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}
