/* agentc, static frontend design system.
   CSS-variable driven, light + dark via prefers-color-scheme. Vanilla, no framework. */

:root {
  /* Visual audit pass 3 (05/09/26, Cat 5 F1): native form-control chrome (checkbox, scrollbar)
     never followed the app's palette. The palette follows the OS (light tokens here, dark ones
     under the prefers-color-scheme block below), so color-scheme follows it too; the forced-dark
     landing and auth pages pin "dark" on body.gm-hero. */
  color-scheme: light dark;
  --color-background: #fafaf9;
  --color-foreground: #18181b;
  --color-surface: #ffffff;
  --color-border: #e7e5e4;
  --color-primary: #4f46e5;
  --color-primary-fg: #ffffff;
  --color-muted: #f5f5f4;
  --color-muted-fg: #57534e;
  --color-danger: #dc2626;
  --color-danger-fg: #fff;
  --color-success: #16a34a;

  --font-size-heading: 1.5rem;
  --font-size-subheading: 0.75rem;
  --font-size-body: 0.9375rem;
  --font-size-label: 0.75rem;
  --font-size-small: 0.8125rem;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --dur-fast: 150ms;
  --dur-base: 220ms;
  --dur-slow: 400ms;
  /* Visual audit pass 3 (05/09/26): three more duration tiers so no rule hardcodes raw ms.
     --dur-micro for sub-150ms feedback, --dur-pulse for the two looping attention pulses
     (integrationHighlight, mnemo-flash), --dur-hero for the landing/auth one-shot entrances. */
  --dur-micro: 100ms;
  --dur-pulse: 800ms;
  --dur-hero: 800ms;
  --radius: 8px;
  /* Visual audit pass 3 (05/09/26, Cat 5 W4): three shadow tiers so ad hoc rgba(0,0,0,x)
     literals reference a token instead of restating the same values. Applied only where the
     existing literal already equalled the tier exactly (never changing a rendered shadow). */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
  --shadow-md: 0 4px 16px rgba(0,0,0,0.14);
  --shadow-lg: 0 24px 60px rgba(0,0,0,0.24);

  /* Soft-keyboard inset (px), driven by visualViewport in js/kb-inset.js. On mobile web this is
     the sole keyboard signal; a native shell also exists now (mobile/, Capacitor, Keyboard.resize:
     "native"), where innerHeight and visualViewport.height shrink together so this stays 0 there
     too. Consumed by #mainContainer.is-mnemo's height calc so the Mnemo composer always tracks
     clear of the keyboard. 0 at rest / desktop. */
  --kb-inset: 0px;

  /* Bottom nav (Slice H, canopy port pattern, js/bottom-nav.js + css/style-04.css there). The
     icon row height is kept separate from the safe-area clearance below it (never squeeze the
     row itself via box-sizing), and the clearance is floored with max() so Android/web
     (env()=0) still get a comfortable gap off the curved bottom edge, the same as a device with
     a real home-indicator inset (see canopy's css/CLAUDE.md "Safe-Area Insets"). */
  --bottom-nav-row: 44px;
  --bottom-nav-gap: 4px;
  --bottom-nav-min-clear: 30px;
  --bottom-nav-clear-h: max(var(--bottom-nav-min-clear), calc(var(--bottom-nav-gap) + env(safe-area-inset-bottom, 0px)));
  --bottom-nav-total-h: calc(var(--bottom-nav-row) + var(--bottom-nav-clear-h));
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #0a0a0a;
    --color-foreground: #f5f5f4;
    --color-surface: #18181b;
    --color-border: #27272a;
    --color-primary: #818cf8;
    --color-primary-fg: #0a0a0a;
    --color-muted: #27272a;
    --color-muted-fg: #a8a29e;
    --color-danger: #f87171;
    --color-success: #4ade80;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

* { box-sizing: border-box; }

/* The [hidden] attribute must always win: a class that sets `display` (e.g. .mnemo-typing
   display:flex, .mnemo-rail-scrim display:block) outspecifies the UA `[hidden]{display:none}`
   rule, which silently strands elements JS toggles via `.hidden` (the Mnemo typing dots animated
   forever because of exactly this). This baseline restores the attribute's intent everywhere. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-background);
  color: var(--color-foreground);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: var(--font-size-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--color-foreground); text-underline-offset: 3px; }
/* Cat 15a/14h (browser W6): an inline <code> run that wraps across lines (source ids like
   SRC_<uuid>) only padded its own start/end without this, leaving mid-run lines flush. */
code {
  background: var(--color-muted); padding: 1px 5px; border-radius: 4px; font-size: 0.85em;
  box-decoration-break: clone; -webkit-box-decoration-break: clone; overflow-wrap: anywhere;
}

/* Layout */
.container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
/* #mainContainer's default top/bottom breathing room for every normal (padded) route: this used
   to be a hard-coded inline style on the <main> tag in space.html, which no external stylesheet
   rule can ever beat on the top/bottom longhands regardless of specificity, so the full-bleed
   #mainContainer.is-mnemo { padding: 0 } rule below could only ever zero out the left/right
   padding (from .container) and never this top/bottom padding. That silently left the #mnemo
   route ~88px short of the real viewport (a dead gap below the composer), which is what let the
   mobile Impact pull-up tab (fixed to the viewport bottom) drift down into the Send button:
   moving the values here, as one shared origin, lets id+class specificity override them properly. */
#mainContainer { padding-top: 28px; padding-bottom: 60px; }
.centered { min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; }
.stack { display: flex; flex-direction: column; gap: 16px; }
.row { display: flex; align-items: center; gap: 12px; }
.between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.muted { color: var(--color-muted-fg); }
.text-small { font-size: var(--font-size-small); }
/* Visual audit pass 3 (05/09/26, Cat 11 W2): the utility tier stopped at .text-small; these two
   complete it against the remaining tokens. */
/* Shared monospace family (Cat 11 W2 + Cat 5 W4): the identity-editor value input (space.js:2104)
   inlined this same stack; also reusable at space.js:483/500. */
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.uppercase-label { font-size: var(--font-size-subheading); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-muted-fg); }
/* --font-size-subheading and --font-size-label are both 0.75rem (identical value), so this
   correction (visual audit pass 3, Cat 11 W2) is purely semantic: the uppercase-divider role
   the subheading token documents now actually uses it, no visible size change. */

/* Screen-reader-only utility (visual audit pass 3, Cat 1 W1 + Cat 4 W1): a visible-nowhere,
   still-focusable-and-announced label. New SoT, first introduced here; both the #mnemo route
   title and intro.js's conversational form labels use it instead of inventing separate names. */
.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;
}

/* Header */
/* Cat 19 (browser W4): safe-area clearance for the top edge, only meaningful once the native
   shell ships with viewport-fit=cover (Capacitor contentInset stays "never"); inert on the web. */
.app-header { border-bottom: 1px solid var(--color-border); background: var(--color-surface); padding-top: env(safe-area-inset-top, 0px); }
.app-header .container { display: flex; align-items: center; justify-content: space-between; height: 56px; flex-wrap: wrap; }
.brand { font-weight: 600; letter-spacing: -0.01em; }
/* A page-level descriptive subtitle beside the brand (e.g. integrations.html) is the first thing
   to drop on a narrow header: it is decoration, not navigation, and a fixed 56px header has no
   room to wrap it without crushing the actual nav links beneath it. */
@media (max-width: 640px) { .app-header .header-subtitle { display: none; } }

/* Bottom Nav (Slice H): THE nav at every width, fixed to the viewport bottom, centered, capped
   on desktop, exactly canopy's geometry. 4 static icon-only routes (no favourites/customisation
   here); markup in space.html, active state + keyboard-hide behaviour in js/space.js. */
.bottom-nav {
  position: fixed; left: 50%; bottom: 0; transform: translateX(-50%);
  width: 100%; max-width: 480px;
  height: var(--bottom-nav-total-h); padding-bottom: var(--bottom-nav-clear-h);
  display: flex; align-items: stretch;
  background: var(--color-surface); border-top: 1px solid var(--color-border);
  /* Above page content, below the rail drawer + scrim (860/850) which sit above it while open. */
  z-index: 800;
}
/* Hidden while a non-modal text field is focused at mobile widths (js/space.js): the fixed bar
   would otherwise sit over the on-screen keyboard covering the field. */
.bottom-nav.kb-hidden { display: none; }

.bottom-nav-item {
  flex: 1; position: relative; display: flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; color: var(--color-muted-fg); text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
  -webkit-tap-highlight-color: transparent; -webkit-user-select: none; user-select: none;
}
.bottom-nav-item svg {
  width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  /* padding was in this list, animating a layout-triggering property on every nav toggle; it now
     snaps instantly with the rest of the box-model change below, and the pop keyframe (a
     transform, never layout-triggering) supplies the felt feedback instead. */
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
/* Active slot: coloured icon on a soft rounded pill behind the glyph (canopy's Duolingo-style
   treatment), never a text label change (icon-only slots). */
.bottom-nav-item.active { color: var(--color-primary); }
.bottom-nav-item.active svg {
  background: var(--color-muted); border-radius: 12px; padding: 6px;
  width: 34px; height: 34px; box-sizing: border-box;
  animation: bottomNavItemPop var(--dur-fast) var(--ease-spring);
}
@keyframes bottomNavItemPop {
  0% { transform: scale(0.82); }
  100% { transform: scale(1); }
}

/* Body clearance so the page-scroll routes (Journal, People and Topics, Tune) never hide content
   behind the fixed bar. Never applied while #mnemo is active: that route sizes its own layout via
   #mainContainer.is-mnemo below and already reserves --bottom-nav-total-h there, so double-adding
   it here would just waste (harmless but pointless) space under an already non-scrolling page. */
body.has-bottom-nav:not(.gm-mnemo-active) { padding-bottom: var(--bottom-nav-total-h); }

/* Card */
.card {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius); padding: 20px; box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: 12px; }
/* 1rem is a deliberate off-scale size (not within ~0.05rem of any --font-size-* tier: nearest is
   --font-size-body at 0.9375rem, a 0.0625rem gap), kept as-is per the audit fix list. */
/* Cat 10f (space-static W8, browser W2): long unbreakable tokens (a person's display name can be
   an email address) otherwise bleed the card's right edge. */
.card-title { font-size: 1rem; font-weight: 500; margin: 0 0 4px; letter-spacing: -0.01em; overflow-wrap: anywhere; }
/* Companion size for auth pages: exactly --font-size-heading, referenced via the token rather
   than restating the literal. */
.card-title-lg { font-size: var(--font-size-heading); }
/* Visual audit pass 3 (05/09/26, Cat 11 W2): every route's bare <h2> was sizing at the UA
   default (1.406rem) instead of the heading token; this is the first rule this file has ever
   defined for the bare element. */
h2 { font-size: var(--font-size-heading); font-weight: 500; letter-spacing: -0.01em; margin: 0 0 4px; }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 40px; padding: 0 16px; border-radius: var(--radius); font-size: var(--font-size-body);
  font-weight: 500; border: 1px solid transparent; cursor: pointer; text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out), transform var(--dur-micro) var(--ease-out), opacity var(--dur-fast);
}
.btn:active { transform: scale(0.98); }
/* Disabled state, native disabled blocks clicks; opacity + cursor make it visually obvious.
   Don't add pointer-events:none here, it would override the not-allowed cursor. */
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: var(--color-primary); color: var(--color-primary-fg); }
.btn-primary:hover { opacity: 0.9; }
.btn-secondary { background: var(--color-surface); color: var(--color-foreground); border-color: var(--color-border); }
.btn-secondary:hover { background: var(--color-muted); }
.btn-danger { background: var(--color-danger); color: var(--color-danger-fg); }
.btn-ghost { background: transparent; color: var(--color-foreground); }
.btn-ghost:hover:not(:disabled) { background: var(--color-muted); }
.btn-ghost-danger { background: transparent; color: var(--color-danger); border-color: var(--color-danger); }
.btn-ghost-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--color-danger) 12%, transparent); }
.btn-small { height: 32px; padding: 0 12px; font-size: var(--font-size-small); }
.btn-full { width: 100%; }

/* Forms */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field label { font-size: var(--font-size-label); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-muted-fg); }
.field label .req { color: var(--color-danger); margin-left: 2px; }
.input, select.input, textarea.input {
  width: 100%; height: 40px; padding: 0 12px; border-radius: var(--radius);
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-foreground); font-size: var(--font-size-body); font-family: inherit;
}
textarea.input { height: auto; min-height: 80px; padding: 10px 12px; resize: vertical; }
/* Custom chevron: the browser default select arrow is a dark-mode risk (it does not follow our
   tokens and can render illegibly against --color-surface in some engines). appearance:none
   strips it; the SVG data-URI chevron below is stroked with the muted-foreground token so it
   reads correctly in both themes, and the extra right padding keeps the arrow clear of text. */
/* An inline SVG data URI cannot reference a CSS custom property, so the stroke color is a plain
   hex that mirrors --color-muted-fg's light value (#57534e); the dark-scheme override below
   mirrors --color-muted-fg's dark value (#a8a29e). Not token-driven, but deliberately kept in
   lockstep with the token pair above, not a stray literal. */
select.input {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 34px;
  background-repeat: no-repeat; background-position: right 12px center; background-size: 14px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none' stroke='%2357534e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M3 5.5L7 9.5L11 5.5'/></svg>");
}
@media (prefers-color-scheme: dark) {
  select.input {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none' stroke='%23a8a29e' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M3 5.5L7 9.5L11 5.5'/></svg>");
  }
}
/* Cat 4/5 (people W3): WebKit's native searchfield chrome (rounded corners, cancel button)
   ignores the border/radius/dark-mode tokens the rest of .input follows. */
.input[type="search"] { -webkit-appearance: none; appearance: none; }
.input[type="search"]::-webkit-search-cancel-button, .input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
.input:focus, select.input:focus, textarea.input:focus {
  outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 25%, transparent);
}

/* Alerts */
.alert {
  padding: 10px 12px; border-radius: var(--radius); font-size: var(--font-size-small); margin: 8px 0;
  animation: mnemoMsgEnter var(--dur-base) var(--ease-out) both;
}
.alert-error { background: color-mix(in srgb, var(--color-danger) 12%, transparent); color: var(--color-danger); }
.alert-success { background: color-mix(in srgb, var(--color-success) 14%, transparent); color: var(--color-success); }

/* Badges */
.badge { display: inline-block; padding: 3px 8px 2px; border-radius: 999px; font-size: var(--font-size-small); background: var(--color-muted); color: var(--color-muted-fg); }

/* Auth pages */
.auth-wrap { width: 100%; max-width: 400px; }

/* ── Slice D reveal (synthesis redesign section 6, orion_disney pass): the nav + shell unfold
   ONCE, right after the intro completes, never on a normal page load. .gm-reveal-pending is set
   synchronously in space.html's <head> before first paint (zero flash); js/space.js flips to
   .gm-reveal-playing on the next frame, which is what actually animates these in, then removes
   both once the sequence finishes. Transform/opacity only (GPU-friendly); prefers-reduced-motion
   (the global block above) collapses every duration here to near-zero, an acceptable
   instant-reveal fallback. Total sequence: header at 0ms, tabs staggered 120 to 240ms, content
   settling from 260ms, done well under the 2.5s budget. */
.gm-reveal-pending .app-header,
.gm-reveal-pending #mainContainer,
.gm-reveal-pending .bottom-nav { opacity: 0; }
.gm-reveal-pending .app-header { transform: translateY(-10px); }
.gm-reveal-pending .bottom-nav { transform: translateY(10px); }
.gm-reveal-pending .bottom-nav-item { opacity: 0; transform: translateY(6px); }
.gm-reveal-pending #mainContainer { transform: translateY(10px); }

.gm-reveal-playing .app-header {
  opacity: 1; transform: translateY(0);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.gm-reveal-playing .brand { animation: gmRevealBrandPop var(--dur-slow) var(--ease-spring) 60ms both; }
@keyframes gmRevealBrandPop {
  0% { transform: scale(0.85); opacity: 0; }
  60% { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.gm-reveal-playing .bottom-nav {
  opacity: 1; transform: translateY(0);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.gm-reveal-playing .bottom-nav-item {
  opacity: 1; transform: translateY(0);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.gm-reveal-playing .bottom-nav-item:nth-child(1) { transition-delay: 120ms; }
.gm-reveal-playing .bottom-nav-item:nth-child(2) { transition-delay: 160ms; }
.gm-reveal-playing .bottom-nav-item:nth-child(3) { transition-delay: 200ms; }
.gm-reveal-playing .bottom-nav-item:nth-child(4) { transition-delay: 240ms; }
.gm-reveal-playing #mainContainer {
  opacity: 1; transform: translateY(0);
  transition: opacity var(--dur-slow) var(--ease-out) 260ms, transform var(--dur-slow) var(--ease-out) 260ms;
}

/* ── SPA route transitions (js/space.js route()): named so startViewTransition captures #content
   as the one swapping surface. Old exits up + blurs, new enters from below + unblurs (a
   continuous spatial story, dossier vt-morph spec), re-expressed in this repo's own tokens. The
   JS guard (API presence, prefers-reduced-motion, same-route no-op) is the entire fallback story:
   an engine without the API, or a reduced-motion user, just gets today's instant swap. ── */
/* Cat 10f (browser W2): a display name or free-text gist can legitimately be a long unbroken
   token (an email address); without this it bleeds past the route's content column. */
#content h1, #content h2, #content h3, #content p { overflow-wrap: anywhere; }
#content { view-transition-name: gm-route-content; }
::view-transition-old(gm-route-content) {
  animation: gmRouteOut var(--dur-base) var(--ease-out) both;
}
::view-transition-new(gm-route-content) {
  animation: gmRouteIn var(--dur-slow) var(--ease-out) 60ms both;
}
@keyframes gmRouteOut { to   { opacity: 0; translate: 0 -12px; filter: blur(4px); } }
@keyframes gmRouteIn  { from { opacity: 0; translate: 0 16px;  filter: blur(4px); } }

/* ── Animated details/summary accordions (native, every plain <details> in the app: the
   source-quote-full excerpt, the person-card Wiki/Identity-map/History expanders, and
   how-i-know; js/space.js:504, 1375, 1378, 1381, 1719 to 1720). interpolate-size makes the
   historically-impossible height:0-to-auto transition native (dossier auto-height-accordion
   spec); engines without it just open instantly, which is exactly today's behavior, so no
   explicit fallback branch is needed. The rotating chevron (down = closed, up = open; a
   border-drawn box, no icon font, no glyph-fallback risk) replaces the native disclosure
   triangle with no markup change. ── */
/* Visual audit pass 3 (05/09/26, Cat 9a F6): kept as a documented exception, not fixed. A
   compositor-only opacity/translate entrance was proposed instead, but it would squash the
   disclosure's own text during the transition; the browser-native height interpolation below is
   the correct tool for this specific case, so it stays. */
:root { interpolate-size: allow-keywords; }
details::details-content {
  transition: height var(--dur-slow) var(--ease-out), content-visibility var(--dur-slow) allow-discrete;
  height: 0;
  overflow: clip;
}
details[open]::details-content { height: auto; }
details > summary { list-style: none; cursor: pointer; }
details > summary::-webkit-details-marker { display: none; }
details > summary::after {
  content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 8px;
  border-right: 1.5px solid var(--color-muted-fg); border-bottom: 1.5px solid var(--color-muted-fg);
  rotate: 45deg; translate: 0 -3px;
  transition: rotate var(--dur-fast) var(--ease-out);
}
details[open] > summary::after { rotate: 225deg; translate: 0 -3px; }

/* Conic confidence ring (dossier conic-ring spec): a progress ring with no SVG, conic-gradient
   hard stop + a radial mask cutting the donut hole. Registering --p via @property is what makes
   it animatable at all (an unregistered custom property is a string and cannot tween); js sets
   the fraction once per render via the inline --p style. The numeric percentage always renders
   alongside it (js/space.js renderProposalCard, renderLearningCard) so the value never depends on
   perceiving the ring alone. */
@property --p { syntax: '<number>'; initial-value: 0; inherits: false; }
.conf-ring-row { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.conf-ring {
  --p: 0;
  display: inline-block; width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;
  background: conic-gradient(var(--color-primary) calc(var(--p) * 1turn), color-mix(in srgb, var(--color-primary) 14%, var(--color-muted)) 0);
  mask: radial-gradient(circle, transparent 54%, black 55%);
  -webkit-mask: radial-gradient(circle, transparent 54%, black 55%);
  transition: --p var(--dur-slow) var(--ease-out);
}

/* ── Landing page showpiece (index.html only; dossier "first-impression surfaces only" tier). ── */

/* Visual audit pass 3 (05/09/26, Cat 1 W1): the base class the h1's own inline style duplicated. */
.gm-hero-title { font-size: clamp(2.75rem, 6vw, 4rem); font-weight: 500; letter-spacing: -0.02em; margin: 0; }

/* masked-rise (dossier spec 1): each word rises out of an overflow:clip mask with a slight
   rotation that straightens on landing, staggered per row. Plays once on load (no repeat-view
   trigger exists on a static splash page); prefers-reduced-motion (the global block above)
   collapses the duration to near-zero, an acceptable instant-reveal fallback, so no separate JS
   branch is needed. */
.gm-hero-title .rise-row { display: inline-block; overflow: clip; vertical-align: top; }
.gm-hero-title .rise-row > span {
  display: inline-block;
  animation: gmHeroRise calc(var(--dur-hero) * 1.4) var(--ease-out) both;
}
.gm-hero-title .rise-row:nth-of-type(2) > span { animation-delay: 0.12s; }
@keyframes gmHeroRise {
  from { translate: 0 110%; rotate: 4deg; }
  to   { translate: 0 0;    rotate: 0deg; }
}

/* pointer aura (dossier spec 9): a large soft radial tint that chases the pointer, lerped in JS
   (index.html's inline script). mix-blend-mode swaps by scheme so the aura reads on both: screen
   only lightens usefully against the dark background, so light mode uses multiply instead (the
   darkening analog) rather than nearly disappearing against `--color-background: #fafaf9`. Starts
   at opacity 0 (no aura at the last-known center before the pointer ever moves); JS sets opacity 1
   on first pointermove. Hidden entirely on coarse (touch) pointers; never started under reduced
   motion (index.html's script checks matchMedia before adding any listener). */
.gm-aura {
  position: fixed; top: 0; left: 0; width: 640px; height: 640px; margin: -320px 0 0 -320px;
  border-radius: 50%; pointer-events: none; z-index: 5; opacity: 0;
  background: radial-gradient(circle, color-mix(in srgb, var(--color-primary) 45%, transparent) 0%, transparent 70%);
  mix-blend-mode: multiply;
  transition: opacity var(--dur-slow) var(--ease-out);
  will-change: transform;
}
@media (prefers-color-scheme: dark) { .gm-aura { mix-blend-mode: screen; } }
@media (pointer: coarse) { .gm-aura { display: none; } }

/* ── Hero scene (index.html + login.html): the generative brainscape backdrop. ───────────────
   A fixed layer stack behind the content: a full-bleed canvas drawn live by js/brainscape.js
   (the wireframe head, its constellation mesh, twinkle and traveling synapse pulses, all from
   data baked out of the original photo, no image requested at runtime), plus a legibility
   scrim (the head sits left, content lives in the darker right half). These two pages force
   the dark palette in BOTH schemes: the scene is dark, so light-mode tokens would render
   unreadable chrome on top of it. */
body.gm-hero {
  color-scheme: dark;
  --color-background: #070f22;
  --color-foreground: #e8edfb;
  --color-surface: #101b3a;
  --color-border: rgba(148, 163, 216, 0.22);
  --color-primary: #818cf8;
  --color-primary-fg: #0a0a1a;
  --color-muted: #16224a;
  --color-muted-fg: #94a3c8;
  --color-danger: #f87171;
  --color-success: #4ade80;
}
body.gm-hero .gm-aura { mix-blend-mode: screen; }

/* Both .gm-scene and .gm-scene-scrim only ever render inside body.gm-hero (index.html,
   login.html), so they inherit that scope's --color-background (#070f22) here instead of
   duplicating the literal; color-mix against transparent reproduces the old rgba alpha stops
   exactly (color-mix(in srgb, C X%, transparent) = the same color at alpha X/100). */
.gm-scene { position: fixed; inset: 0; z-index: 0; overflow: clip; background: var(--color-background); }
/* Legibility scrim: fade the busy left half into the page navy on the right (where the copy and
   card sit), plus a gentle bottom vignette so the footer text never fights the particles. */
.gm-scene-scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--color-background) 10%, transparent) 0%,
      color-mix(in srgb, var(--color-background) 42%, transparent) 46%,
      color-mix(in srgb, var(--color-background) 88%, transparent) 74%,
      var(--color-background) 100%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--color-background) 25%, transparent) 0%,
      transparent 30%, transparent 62%,
      color-mix(in srgb, var(--color-background) 75%, transparent) 100%);
}
.gm-scene-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Content sits above the scene; right-aligned on wide screens (the image's empty half), centered
   on narrow ones where the scrim goes full-dark instead. */
.gm-hero-layout {
  position: relative; z-index: 1; min-height: 100vh; min-height: 100dvh;
  display: flex; align-items: center;
}
.gm-hero-copy { margin-left: auto; max-width: 560px; }
.gm-hero-copy .gm-hero-title { text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45); }
.gm-fade-up { opacity: 0; animation: gmFadeUp var(--dur-hero) var(--ease-out) both; }
/* Visual audit pass 3 (05/09/26, Cat 9d/9e W2): the stagger ladder, moved out of index.html's
   inline animation-delay attributes into the one place that already owns this sequence's timing.
   Targeted by each element's own distinguishing class rather than :nth-of-type, which counts
   per-tag-name and would misgroup this row's mixed p/div/p siblings. */
.gm-hero-lede.gm-fade-up { animation-delay: 250ms; }
.gm-hero-copy > p.text-small.gm-fade-up:not(.mt-2) { animation-delay: 400ms; }
#cta.gm-fade-up { animation-delay: 550ms; }
.gm-hero-copy > p.text-small.mt-2.gm-fade-up { animation-delay: 700ms; }
/* index.html W1: the inline font-size on the hero lede, off the token scale but a deliberate
   display-adjacent size (nearest token --font-size-body at 0.9375rem). */
.gm-hero-lede { font-size: 1.1rem; }
@keyframes gmFadeUp {
  from { opacity: 0; translate: 0 16px; }
  to   { opacity: 1; translate: 0 0; }
}

/* Login card: glass over the scene. The entrance is a single settle (rise + fade), then static. */
body.gm-hero .auth-wrap { margin-left: auto; }
body.gm-hero .auth-card {
  /* Cat 5 F1 (login report): these three rgba literals restated the body.gm-hero token values
     (--color-surface at 58%, --color-border exactly, --color-background at 55%) instead of
     referencing them, the way this same file already does at .gm-scene-scrim. */
  background: color-mix(in srgb, var(--color-surface) 58%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.25);
  backdrop-filter: blur(18px) saturate(1.25);
  border: 1px solid var(--color-border);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
  padding: 28px;
  animation: gmCardEnter var(--dur-hero) var(--ease-out) both;
}
@keyframes gmCardEnter {
  from { opacity: 0; translate: 0 22px; }
  to   { opacity: 1; translate: 0 0; }
}
body.gm-hero .auth-card .input { background: color-mix(in srgb, var(--color-background) 55%, transparent); }
.gm-wordmark {
  display: inline-block; text-decoration: none; margin-bottom: 14px;
  font-size: var(--font-size-label); text-transform: uppercase; letter-spacing: 0.16em;
  color: var(--color-muted-fg);
}

@media (max-width: 860px) {
  /* Narrow: brainscape.js re-frames the scene itself (portrait crops center on the face), so
     the scrim only has to dim it top-to-bottom and the content centers over it. */
  .gm-scene-scrim {
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--color-background) 42%, transparent) 0%,
        color-mix(in srgb, var(--color-background) 62%, transparent) 45%,
        color-mix(in srgb, var(--color-background) 92%, transparent) 100%);
  }
  .gm-hero-copy, body.gm-hero .auth-wrap { margin-left: auto; margin-right: auto; }
}

/* Empty state */
.empty { color: var(--color-muted-fg); font-size: var(--font-size-small); padding: 8px 0; }
/* ui-state.js's emptyHtml() renders `<p class="empty empty-state-text">`, the "nothing here yet"
   message specifically (loading uses `.empty` alone, with a spinner, no second class); it carried
   zero rules of its own before this pass, borrowing 100% of its look from `.empty` with the
   second class name doing nothing. Web audit follow-up (05/09/26): centered padding so the
   message reads as a deliberate placeholder rather than a left-aligned stray line inside an
   otherwise empty list/card. */
.empty-state-text { text-align: center; padding: 16px 8px; }

/* Timeline */
.event { display: flex; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--color-border); }
.event pre { margin: 4px 0 0; font-size: var(--font-size-label); color: var(--color-muted-fg); white-space: pre-wrap; word-break: break-word; }

/* Rendered markdown (person wiki). Compact rhythm so a sectioned profile reads like a page,
   not a form. Fed only by the allow-list renderer in js/markdown.js. */
.markdown-body { font-size: var(--font-size-small); line-height: 1.55; }
/* 0.9rem and 0.82rem were within 0.05rem of --font-size-body (0.9375rem) and --font-size-small
   (0.8125rem) respectively; snapped to the tokens, no visible size change. */
.markdown-body h2 { font-size: var(--font-size-body); font-weight: 600; margin: 14px 0 4px; }
.markdown-body h3, .markdown-body h4 { font-size: var(--font-size-small); font-weight: 600; margin: 12px 0 4px; }
.markdown-body h2:first-child, .markdown-body h3:first-child { margin-top: 0; }
.markdown-body p { margin: 4px 0; }
.markdown-body ul, .markdown-body ol { margin: 4px 0 4px 18px; padding: 0; }
.markdown-body li { margin: 2px 0; }
.markdown-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; background: var(--color-muted); padding: 1px 4px; border-radius: 4px; }

/* Utility spacing */
.mt-2 { margin-top: 8px; } .mt-4 { margin-top: 16px; } .mt-6 { margin-top: 24px; } .mb-2 { margin-bottom: 8px; }
/* Visual audit pass 3 (05/09/26, browser F2): referenced across space.js (pill sub-tab rows,
   person page back links) but never defined; every use fell back to 0px. */
.mb-4 { margin-bottom: 16px; }
.grid-2 { display: grid; gap: 24px; grid-template-columns: 1fr 320px; }
@media (max-width: 720px) { .grid-2 { grid-template-columns: 1fr; } }
.spinner { display:inline-block; width:14px; height:14px; border:2px solid var(--color-border); border-top-color: var(--color-primary); border-radius:50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Attention tab (Phase F3, 2026-07-11 redesign): engagement summary strip, filter cards with
   editable keyword chips, active/archived toggle pills, and the "what grey matter learnt"
   click-through panel. */
.stat-strip { display: flex; gap: 12px; flex-wrap: wrap; }
.stat-chip {
  flex: 1; min-width: 160px; background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius); padding: 12px 16px; display: flex; flex-direction: column; gap: 2px;
}
.stat-chip-value { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.01em; }
.stat-chip-label { font-size: var(--font-size-label); color: var(--color-muted-fg); }

.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }

/* Address book (Identity map editor). One card per app; each row is a human sentence with a
   status chip + actions, the raw key (UUID/JID) demoted to a fine-print line below, and the
   card's own inline add control at the bottom. Address-book matches collapse into a details
   expander with a one-line summary. */
.pid-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 12px; align-items: start;
}
.pid-card .card-title { margin-bottom: 6px; }
.pid-abook summary { cursor: pointer; }
.pid-abook .pid-row:first-of-type { border-top: 1px solid var(--color-border); }
.pid-row { padding: 8px 0; border-top: 1px solid var(--color-border); }
.pid-row:first-of-type { border-top: none; }
/* Visual audit pass 3 (05/09/26, Cat 1 W6): the same hand-written "list row with a top divider"
   inline style existed at three call sites in space.js; this is the one shared rule .pid-row
   above already was. */
.list-row { padding: 8px 0; border-top: 1px solid var(--color-border); }
.list-row.between { gap: 8px; }
.page-body-text { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-border); white-space: pre-wrap; }
.pid-main { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.pid-sentence { font-size: var(--font-size-small); min-width: 0; overflow-wrap: anywhere; }
.pid-fine {
  margin-top: 2px; font-size: var(--font-size-label); color: var(--color-muted-fg);
  font-family: var(--font-mono, ui-monospace, monospace); overflow-wrap: anywhere;
}
.pid-actions { display: flex; gap: 4px; flex-wrap: wrap; }
.pid-add { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pid-add .pid-add-value { flex: 1; min-width: 140px; }
.pid-review-badge { background: color-mix(in srgb, var(--color-primary) 14%, transparent); color: var(--color-primary); }
.chip {
  display: inline-flex; align-items: center; gap: 4px; background: var(--color-muted);
  color: var(--color-foreground); border-radius: 999px; padding: 3px 6px 3px 10px; font-size: var(--font-size-small);
  /* Cat 10f (browser F4): chips render identity strings (JIDs, emails) that can legitimately be
     one unbreakable token; without this a long one bleeds the card's right edge. */
  max-width: 100%; min-width: 0; overflow-wrap: anywhere;
}
/* Cat 12a (space-static F3): the app-key scope picker's checkbox-in-a-chip pattern; no inline
   style, no forced height (its own content plus the mobile touch-target rule below decide that). */
.chip-check { cursor: pointer; }
.chip-check input { margin: 0 4px 0 0; }
.chip-remove {
  position: relative;
  border: none; background: transparent; color: var(--color-muted-fg); cursor: pointer;
  font-size: 0.95em; line-height: 1; padding: 2px 4px; border-radius: 999px;
}
.chip-remove:hover { color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 12%, transparent); }
/* Cat 10c (space-static F5): a 44px hit area without inflating the visible chip glyph (which
   would also inflate every sibling chip using the same rule). */
.chip-remove::before { content: ""; position: absolute; inset: -10px; }

.toggle-pill {
  border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-muted-fg);
  border-radius: 999px; padding: 4px 12px; font-size: var(--font-size-small); font-weight: 500;
  cursor: pointer; white-space: nowrap; transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.toggle-pill.is-active { color: var(--color-success); border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border)); }
.toggle-pill:hover { background: var(--color-muted); }
/* Touch target fix: at mobile widths .toggle-pill's own content-driven height falls short of the
   44px minimum touch target. Widened padding compensates so the pill still reads visually
   balanced (not just a tall skinny box) at the larger min-height. */
@media (max-width: 767px) {
  .toggle-pill { min-height: 44px; padding: 4px 16px; display: inline-flex; align-items: center; }
  /* Visual audit pass 3 (05/09/26, Cat 10c/12c F4/F5, browser W7): every other control the three
     reports flagged under 44px at phone width, fixed once here rather than per-class. */
  .btn-small, .chip-check { min-height: 44px; height: auto; }
  .mnemo-loop-chip, .mnemo-receipt, .mnemo-loops-more {
    min-height: 44px; padding-top: 10px; padding-bottom: 10px; display: inline-flex; align-items: center;
  }
  .pin-btn, .mnemo-answer-chip-clear, .mnemo-rail-close { min-width: 44px; min-height: 44px; }
}

.badge-protected { background: color-mix(in srgb, var(--color-primary) 16%, transparent); color: var(--color-primary); }

/* Journal (learning cards feed, Slice B synthesis redesign) */
.journal-card .card-status { margin-bottom: 0; }
/* Cat 10f (browser W1): under a long unbreakable neighbour, the status badge was squeezed to a
   one-glyph column instead of keeping its own natural width. */
.badge.card-status { flex-shrink: 0; }
.card-status-unconfirmed { background: var(--color-muted); color: var(--color-muted-fg); }
.card-status-confirmed { background: color-mix(in srgb, var(--color-success) 16%, transparent); color: var(--color-success); }
.card-status-corrected { background: color-mix(in srgb, var(--color-primary) 16%, transparent); color: var(--color-primary); }
.card-status-retired, .card-status-merged { background: color-mix(in srgb, var(--color-danger) 12%, transparent); color: var(--color-danger); }
/* Slice G: a contradiction flag stamped into facts_json (_shared/card-core.ts's
   flagContradiction), reusing the plain fact list with no schema change. */
.fact-needs-review { color: var(--color-danger); font-weight: 600; }
.journal-card .fix-box, .journal-card .sources-box {
  border-top: 1px solid var(--color-border); padding-top: 12px;
}
.journal-card .fix-box select.input { height: 36px; }
.merge-proposal .badge { background: color-mix(in srgb, var(--color-primary) 16%, transparent); color: var(--color-primary); }

/* Sources expander evidence (owner complaint, 2026-07-12): each source_refs entry hydrates into
   a quoted snippet + a quiet meta line, never raw JSON or an id. */
.source-evidence + .source-evidence { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border); }
.source-quote {
  margin: 0; padding: 6px 10px; border-left: 2px solid var(--color-border);
  font-size: var(--font-size-small); font-style: italic; color: var(--color-foreground);
}
.source-quote-full summary { cursor: pointer; margin-top: 4px; }
.source-quote-full p { font-style: normal; }
.source-doc-title { margin: 0 0 4px; }
.source-meta { margin: 4px 0 0; }
.source-missing { font-style: italic; }

/* People index (2026-07-18 redesign): searchable minimal name+gist cards, click-through to
   #person/<id>, star pin (up to 3) with the owner's own card always first. */
.people-search { max-width: 420px; }
.person-card { position: relative; cursor: pointer; transition: border-color var(--dur-fast, 120ms) var(--ease-out); }
.person-card:hover, .person-card:focus-visible { border-color: var(--color-primary); outline: none; }
.person-card .card-title { margin: 0; padding-right: 36px; }
.person-card-gist {
  margin-top: 6px; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.person-card-placeholder { cursor: default; }
.pin-btn {
  position: absolute; top: 10px; right: 10px; z-index: 1;
  min-width: 32px; min-height: 32px; border: none; background: transparent;
  /* 1.05rem is a deliberate off-scale size for the pin glyph itself (not within ~0.05rem of any
     --font-size-* tier), kept as-is per the audit fix list. */
  color: var(--color-border); font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), transform var(--dur-micro) var(--ease-out);
}
.pin-btn:hover { color: var(--color-muted-fg); }
.pin-btn:active { transform: scale(0.85); }
.pin-btn.is-pinned { color: var(--color-primary); }
/* Pin micro-animations (Disney pass, 2026-07-18): pinning is a deliberate, occasional act, so
   the star earns a small spring pop (anticipation released); unpinning is routine bookkeeping,
   so it gets a quieter dip. The card's travel to/from the pinned shelf is a View Transition
   (see loadPeople's pin handler): names are assigned per-card only around that one re-render,
   so route changes never inherit per-card groups. Transform-only; the global
   prefers-reduced-motion block collapses both keyframes. */
@keyframes pinPop {
  0% { transform: scale(1); }
  45% { transform: scale(1.35); }
  100% { transform: scale(1); }
}
.pin-btn.is-popping { animation: pinPop var(--dur-slow) var(--ease-spring); }
@keyframes pinDip {
  0% { transform: scale(1); }
  50% { transform: scale(0.8); }
  100% { transform: scale(1); }
}
.pin-btn.is-dipping { animation: pinDip var(--dur-fast) var(--ease-out); }
.wiki-section { margin-top: 10px; }
.wiki-section:first-child { margin-top: 0; }
.how-i-know summary { list-style: none; }
.how-i-know summary::-webkit-details-marker { display: none; }
.card-cite-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 8px; border-radius: 6px; text-decoration: none; color: inherit;
  border: 1px solid var(--color-border);
}
.card-cite-row:hover { background: var(--color-muted); }
.provenance-strip { display: flex; flex-direction: column; gap: 6px; }
.provenance-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 8px; border-radius: 6px; border: 1px solid var(--color-border);
}

input[type="range"], input[type="checkbox"], input[type="radio"] { accent-color: var(--color-primary); }

/* Integrations page: a brain-in-the-middle flow composition. Three columns on desktop (in / core
   / out); the CORE stays visually central and equal-weighted so the page reads as one designed
   object, not three stacked card lists. Below ~900px it stacks vertically in reading order
   (in, core, out), matching how the flow is meant to be read regardless of viewport. */
.integration-flow {
  display: grid; grid-template-columns: 1fr 200px 1fr; gap: 28px;
}
@media (max-width: 900px) {
  .integration-flow { grid-template-columns: 1fr; }
  .flow-core { order: -1; }
}
/* The core column stretches to match its tallest sibling (the flanking card columns) so the
   connecting spine below can run the full length: without this, the column's own box shrinks to
   its content height, leaving a visually accidental void beneath it rather than a deliberate one.
   The visible content (orb + caption) still pins near the top via sticky; the spine is a second,
   independent layer filling the stretched box, so scrolling never separates the two. */
.flow-core {
  display: flex; flex-direction: column; align-items: center; position: relative;
}
/* The flow spine: a soft vertical line threading through the core column, turning what would
   otherwise be dead center-column whitespace into a deliberate "this is one pipeline" cue,
   matching the brain-in-the-middle concept (sources flow in, through the core, out to apps). */
.flow-core::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; transform: translateX(-50%);
  background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--color-primary) 35%, transparent) 15%, color-mix(in srgb, var(--color-primary) 35%, transparent) 85%, transparent);
  z-index: 0;
}
@media (max-width: 900px) { .flow-core { position: static; } .flow-core::before { display: none; } }
.flow-core-sticky {
  position: sticky; top: 24px; padding: 20px 12px; display: flex; flex-direction: column; align-items: center;
  z-index: 1; background: var(--color-background);
}
@media (max-width: 900px) { .flow-core-sticky { position: static; } }
.core-orb {
  width: 108px; height: 108px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-primary) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  position: relative; z-index: 1;
}
/* 1.1rem is a deliberate off-scale size for the flow-diagram arrow glyph (not within ~0.05rem of
   any --font-size-* tier), kept as-is per the audit fix list. */
.flow-arrow {
  position: absolute; top: 60px; font-size: 1.1rem; color: color-mix(in srgb, var(--color-primary) 55%, var(--color-muted-fg));
  z-index: 1;
}
.flow-arrow.flow-arrow-in { left: -22px; }
.flow-arrow.flow-arrow-out { right: -22px; }
@media (max-width: 900px) { .flow-arrow { display: none; } }

.integration-card { position: relative; }
.integration-card.is-mnemo { border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); background: color-mix(in srgb, var(--color-primary) 5%, var(--color-surface)); }
/* Renamed integration-channel to integration-workspace (05/09/26, W7 SAFE rename): the markup in
   integrations.js carries the new name; no alias remains. */
.integration-workspace {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 0; border-top: 1px solid var(--color-border);
}
.integration-workspace:first-of-type { border-top: none; }
/* Cat 10f (space-static W8): the text column had no flex/min-width, so a long label could push
   the Reconnect/Disconnect button out past the card edge instead of wrapping. */
.integration-workspace > div:first-child { flex: 1; min-width: 0; }
/* margin-top (not padding on the badge above it): every usage (grep js/integrations.js) has
   .integration-meta as the sibling line directly under a label/badge row inside the same card,
   so this closes the sub-4px browser-measured gap without touching the badge's own padding or
   risking misalignment elsewhere. */
.integration-meta { font-size: var(--font-size-small); color: var(--color-muted-fg); margin-top: 4px; }

/* How it works page: inline SVG diagrams, boxed to the app's card/surface language. */
.diagram-wrap { position: relative; overflow-x: auto; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 12px; }
.diagram-wrap svg { display: block; width: 100%; height: auto; min-width: 560px; }
/* is-scrollable is set by how-it-works.html's own script when a diagram is wider than its
   container (always true below about 630px). The fade signals more content sits off-screen;
   the paired .diagram-hint text makes the same fact explicit for anyone who won't notice a
   gradient, screen readers included. */
.diagram-wrap.is-scrollable::after {
  content: ""; position: absolute; top: 12px; right: 0; bottom: 12px; width: 28px;
  background: linear-gradient(to right, transparent, var(--color-surface));
  border-radius: 0 var(--radius) var(--radius) 0; pointer-events: none;
}
.diagram-hint { display: none; font-size: var(--font-size-label); color: var(--color-muted-fg); text-align: center; margin-top: 6px; }
.diagram-wrap.is-scrollable + .diagram-hint { display: block; }
.howitworks-section { margin-top: 40px; }
.howitworks-section h2 { margin-bottom: 6px; }
.howitworks-nav { display: flex; gap: 16px; flex-wrap: wrap; font-size: var(--font-size-small); }
.howitworks-nav a { color: var(--color-muted-fg); text-decoration: none; }
.howitworks-nav a:hover { color: var(--color-foreground); }

/* Diagram primitives shared by every inline SVG on how-it-works.html: plain boxes, pills,
   arrows, and label text, all theme-aware via the same CSS variables as the rest of the app. */
.dg-box { fill: var(--color-surface); stroke: var(--color-border); stroke-width: 1.5; }
.dg-box-accent { fill: var(--color-surface); stroke: var(--color-primary); stroke-width: 1.5; }
.dg-pill { fill: var(--color-muted); stroke: none; }
.dg-pill-hit { fill: color-mix(in srgb, var(--color-success) 16%, var(--color-muted)); stroke: none; }
.dg-pill-drop { fill: color-mix(in srgb, var(--color-danger) 12%, var(--color-muted)); stroke: none; }
.dg-arrow { stroke: var(--color-muted-fg); stroke-width: 2; fill: none; }
.dg-lbl {
  fill: var(--color-foreground); text-anchor: middle; dominant-baseline: middle; font-size: 13px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.dg-lbl-sm { fill: var(--color-muted-fg); text-anchor: middle; dominant-baseline: middle; font-size: 11px; font-family: inherit; }
.dg-lbl-title {
  fill: var(--color-muted-fg); text-anchor: middle; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* ── Mnemo chat page (Phase H3): friend-like bubble thread (left) + ripple/impact rail
   (right on desktop, a pull-up drawer behind an "Impact" tab on mobile). ── */

.mnemo-layout {
  display: flex; align-items: stretch;
  height: 100%; min-height: 0; /* #mainContainer.is-mnemo below sizes this to the real viewport-minus-header */
  max-width: 1300px; margin: 0 auto;
}

/* Slice C shell: the #mnemo route wants the full remaining viewport (no container max-width or
   padding fighting mnemo-layout's own internal layout); every other route keeps the normal
   padded container. --header-h is measured live by js/space.js (the header's own row count can
   change between narrow/wide viewports as .app-header .container wraps), falling back to a
   reasonable two-row default before the first measurement lands.
   The static-frame fix (canopy/chatbox messenger pattern): #content has no explicit height of
   its own, so a plain `height: 100%` on .mnemo-layout used to resolve against an auto-height
   ancestor and collapse to auto, letting the whole chat column grow to its content's natural
   height instead of clipping at the viewport. #mainContainer.is-mnemo now becomes the flex
   parent that hands #content a DEFINITE height (flex layout, not a percentage), and clips
   anything that still slips past it; body.gm-mnemo-active (toggled by js/space.js's route())
   is the belt-and-braces guard against the page itself ever scrolling on this route. */
/* Slice H: also subtracts --bottom-nav-total-h (the bar stays visible on Mnemo, top-level nav,
   not hidden like canopy's messenger) and --kb-inset (the composer tracks the soft keyboard
   instead of the fixed-height chat pane sitting under it; 0 at rest / desktop). */
#mainContainer.is-mnemo {
  max-width: none; padding: 0;
  height: calc(100vh - var(--header-h, 101px) - var(--bottom-nav-total-h) - var(--kb-inset, 0px));
  display: flex; flex-direction: column; overflow: hidden;
}
#mainContainer.is-mnemo > #content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
body.gm-mnemo-active { overflow: hidden; }

.mnemo-chat {
  flex: 2 1 640px; min-width: 0; min-height: 0; display: flex; flex-direction: column;
  border-right: 1px solid var(--color-border);
}

.mnemo-messages {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 20px 24px 8px;
  display: flex; flex-direction: column; gap: 2px;
}

.mnemo-load-older-row { align-self: center; margin: 4px 0 10px; }

.mnemo-date-sep {
  align-self: center; margin: 14px 0 6px; padding: 3px 10px; border-radius: 999px;
  background: var(--color-muted); color: var(--color-muted-fg);
  font-size: var(--font-size-label); text-transform: uppercase; letter-spacing: 0.06em;
}

.mnemo-row { display: flex; margin: 2px 0; }
.mnemo-row.is-mnemo { justify-content: flex-start; }
.mnemo-row.is-you { justify-content: flex-end; }
/* Burst grouping: consecutive messages from the same sender sit close together; the gap widens
   only when the sender changes or a date separator intervenes (the visual cue for "a burst"). */
.mnemo-row + .mnemo-row.is-mnemo, .mnemo-row + .mnemo-row.is-you { margin-top: 2px; }
.mnemo-row.mnemo-group-start { margin-top: 12px; }

.mnemo-bubble {
  max-width: 72%; padding: 9px 13px; border-radius: 16px; font-size: var(--font-size-body);
  line-height: 1.4; white-space: pre-wrap; word-break: break-word;
}
.mnemo-bubble-mnemo {
  background: var(--color-muted); color: var(--color-foreground); border-bottom-left-radius: 4px;
}
.mnemo-bubble-you {
  background: var(--color-primary); color: var(--color-primary-fg); border-bottom-right-radius: 4px;
}

/* Open loops as conversation: one synthetic, client-rendered Mnemo bubble at the bottom of the
   thread (never posted to Switchboard), replacing the old stacked "Open threads" panel. Slightly
   distinct styling (dashed border, faint tint) so it reads as a live, disappearing surface rather
   than settled history; it prunes in place as loops close and vanishes entirely when empty. */
.mnemo-bubble-loops {
  max-width: 84%; border: 1px dashed color-mix(in srgb, var(--color-primary) 35%, var(--color-border));
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-muted));
  /* Cat 14 (browser F1): .mnemo-bubble's pre-wrap renders the loops template literal's own
     newlines/indentation as visible blank lines; this bubble's content is markup-driven list
     items, not free text, so it never needed pre-wrap in the first place. */
  white-space: normal;
}
.mnemo-loops-intro { font-size: var(--font-size-small); margin-bottom: 6px; }
.mnemo-loops-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.mnemo-loop-line {
  display: flex; flex-direction: column; align-items: stretch; gap: 6px;
  padding: 6px 0; border-top: 1px solid color-mix(in srgb, var(--color-primary) 16%, transparent);
}
.mnemo-loop-line:first-child { border-top: none; }
.mnemo-loop-summary { font-size: var(--font-size-small); min-width: 0; overflow-wrap: anywhere; }
.mnemo-loop-count { color: var(--color-muted-fg); font-size: var(--font-size-label); }
.mnemo-loop-actions { display: flex; gap: 4px; flex-wrap: wrap; }
.mnemo-loop-chip {
  border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-foreground);
  border-radius: 999px; padding: 2px 9px; font-size: var(--font-size-label); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.mnemo-loop-chip:hover { background: var(--color-muted); }
.mnemo-loops-more {
  display: block; margin-top: 6px; border: none; background: transparent; color: var(--color-muted-fg);
  font-size: var(--font-size-label); cursor: pointer; padding: 2px 0; text-decoration: underline;
}

.mnemo-answer-chip {
  display: flex; align-items: center; gap: 8px; margin: 0 16px 6px; padding: 6px 10px;
  border-radius: 10px; border: 1px dashed color-mix(in srgb, var(--color-primary) 35%, var(--color-border));
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-muted));
  font-size: var(--font-size-label); color: var(--color-muted-fg);
}
.mnemo-answer-chip-label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mnemo-answer-chip-clear {
  flex-shrink: 0; border: none; background: transparent; color: var(--color-muted-fg);
  cursor: pointer; font-size: var(--font-size-body); line-height: 1; padding: 2px 4px;
}

/* Impact receipt: a quiet chip under a "you" bubble whose assistant_message_id shows up in
   outcome_events. Collapsed by default; expands to list the outcomes inline on tap. */
.mnemo-receipt {
  align-self: flex-end; margin: 2px 4px 8px; display: inline-flex; align-items: center; gap: 4px;
  background: transparent; border: 1px solid var(--color-border); color: var(--color-muted-fg);
  border-radius: 999px; padding: 2px 10px; font-size: var(--font-size-label); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.mnemo-receipt:hover { background: var(--color-muted); color: var(--color-foreground); }
.mnemo-receipt-detail {
  align-self: flex-end; max-width: 72%; margin: 0 4px 10px; padding: 8px 12px;
  background: var(--color-muted); border-radius: var(--radius); font-size: var(--font-size-small);
}
.mnemo-receipt-detail ul { margin: 4px 0 0 18px; padding: 0; }
.mnemo-receipt-detail li { margin: 2px 0; }

.mnemo-typing {
  display: flex; gap: 4px; padding: 4px 24px 8px; align-items: center;
}
.mnemo-typing-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--color-muted-fg);
  animation: mnemo-typing-bounce 1.1s var(--ease-out) infinite;
}
.mnemo-typing-dot:nth-child(2) { animation-delay: 0.15s; }
.mnemo-typing-dot:nth-child(3) { animation-delay: 0.3s; }
@keyframes mnemo-typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-4px); opacity: 1; }
}

.mnemo-composer {
  display: flex; align-items: flex-end; gap: 10px; padding: 12px 20px;
  border-top: 1px solid var(--color-border); background: var(--color-surface);
}
/* textarea.mnemo-input (not .mnemo-input alone): the generic textarea.input rule above
   (min-height: 80px, for free-text areas elsewhere) outranks a bare class selector on
   specificity regardless of source order, which was silently forcing this composer to 80px
   and pushing the mobile Impact rail tab (fixed, tuned for a ~64 to 70px composer) down into
   the Send button. Matching its type+class specificity lets this rule win by cascade order. */
/* field-sizing: content (supporting engines) grows the composer with typed content between
   min/max-height with no JS at all; js/space.js's autosizeMnemoInput() checks CSS.supports and
   no-ops there, staying the fallback for engines that don't support it yet. Same 140px cap
   either way. */
textarea.mnemo-input {
  flex: 1; height: auto; min-height: 40px; max-height: 140px; padding: 9px 12px; resize: none; line-height: 1.4;
  field-sizing: content;
}
.mnemo-send { flex-shrink: 0; }

.mnemo-rail {
  flex: 1 1 300px; max-width: 320px; min-height: 0; padding: 20px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
}
.mnemo-rail-header { display: none; align-items: center; justify-content: space-between; }
/* 1.4rem is a deliberate off-scale size for the close glyph (not within ~0.05rem of any
   --font-size-* tier), kept as-is per the audit fix list. */
.mnemo-rail-close {
  border: none; background: transparent; color: var(--color-muted-fg); font-size: 1.4rem;
  line-height: 1; cursor: pointer; padding: 2px 6px;
}
.mnemo-recap { margin: 0; }

.mnemo-ripple-list { display: flex; flex-direction: column; gap: 8px; }
.mnemo-ripple-card {
  display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px;
  background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
  cursor: pointer; text-align: left;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.mnemo-ripple-card:hover { background: var(--color-muted); border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border)); }
.mnemo-ripple-icon {
  flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: var(--color-muted);
  display: flex; align-items: center; justify-content: center; font-size: 14px;
}
.mnemo-ripple-body { min-width: 0; }
.mnemo-ripple-text { font-size: var(--font-size-small); margin: 0; overflow-wrap: anywhere; }
.mnemo-ripple-time { font-size: var(--font-size-label); color: var(--color-muted-fg); }

/* Flash highlight when a ripple card scrolls the chat to its causing bubble. */
.mnemo-bubble.is-flashed { animation: mnemo-flash var(--dur-pulse) var(--ease-out); }
@keyframes mnemo-flash {
  0% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 55%, transparent); }
  100% { box-shadow: 0 0 0 3px transparent; }
}

.mnemo-empty-state { padding: 32px 12px; text-align: center; }
.mnemo-empty-state p { margin: 0; max-width: 34ch; margin: 0 auto; }

.mnemo-rail-tab { display: none; }
.mnemo-rail-scrim { display: none; }

/* Slice H: 640px is the app's one breakpoint family (matches the messenger-precedent family
   elsewhere in the fleet); this used to be its own 860px boundary, now unified. */
@media (max-width: 640px) {
  .mnemo-layout { max-width: none; } /* height stays 100% of #mainContainer.is-mnemo, --header-h aware */
  .mnemo-chat { border-right: none; }
  /* Cat 20 (browser W5): the absolutely-positioned Impact tab floats over the top of the message
     list at this width; padding the list clears the newest-scrolled bubble underneath it. */
  .mnemo-messages { padding-top: 52px; }
  /* Docks its bottom at the bottom-nav's TOP edge (not the viewport bottom), so the open drawer
     sits neatly above the bar instead of overlapping or fighting it for z-index. */
  .mnemo-rail {
    position: fixed; left: 0; right: 0; bottom: var(--bottom-nav-total-h); max-width: none; z-index: 860;
    background: var(--color-surface); border-top: 1px solid var(--color-border);
    /* Kept literal (not --shadow-md, Cat 5 W4 deviation): this is an upward drop shadow for a
       drawer docked at the bottom edge; remapping to --shadow-md would flip its Y offset positive
       and change the rendered direction, not just tokenize the value. */
    border-radius: 16px 16px 0 0; box-shadow: 0 -4px 20px rgba(0,0,0,0.12);
    max-height: 72vh;
    /* Closed state: translateY(100%) alone (the pre-Slice-H value) shifted the box down by only
       its OWN height, which fully cleared the viewport back when this docked flush at bottom:0.
       Now that it docks bottom:var(--bottom-nav-total-h) (above the bar, never overlapping it),
       a SHORT rail (little content) no longer clears: its own height alone leaves a
       --bottom-nav-total-h-tall sliver of its top edge peeking in behind the bottom nav. Shifting
       the extra distance closes that gap regardless of the rail's rendered height. */
    transform: translateY(calc(100% + var(--bottom-nav-total-h)));
    transition: transform var(--dur-slow) var(--ease-out);
  }
  .mnemo-rail.is-open { transform: translateY(0); }
  .mnemo-rail-header { display: flex; }
  /* Impact toggle: rehomed (Slice H) to a chip at the thread's own top-right corner instead of a
     viewport-fixed pill hovering near the composer. Anchored inside .mnemo-chat (position:
     relative, see the Disney-pass rule below), so it can never overlap the composer OR the
     bottom nav, at any composer height, keyboard state, or bottom-nav visibility. */
  .mnemo-rail-tab {
    display: block; position: absolute; top: 12px; right: 16px; z-index: 5;
    border: 1px solid var(--color-border); background: var(--color-primary); color: var(--color-primary-fg);
    border-radius: 999px; padding: 8px 16px; font-size: var(--font-size-small); font-weight: 500;
    /* Kept literal (not --shadow-md, Cat 5 W4 deviation): blur (10px vs 16px) and alpha (0.18 vs
       0.14) both differ from the token; remapping would change this pill's rendered shadow, not
       just tokenize it. */
    cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.18);
  }
  .mnemo-rail-scrim {
    /* Cat 5 W4: derives from --color-background like .gm-confirm-overlay already does, instead
       of a hardcoded black, so the scrim reads correctly whichever theme is active. */
    display: block; position: fixed; inset: 0; background: color-mix(in srgb, var(--color-background) 55%, transparent); z-index: 850;
    opacity: 0; pointer-events: none; transition: opacity var(--dur-base) var(--ease-out);
  }
  .mnemo-rail-scrim.is-open { opacity: 1; pointer-events: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .mnemo-typing-dot { animation: none; opacity: 0.7; }
}

/* ── Intro flow (Slice D, synthesis redesign): full-screen, Mnemo-voiced, conversational.
   Reuses the Mnemo chat page's own bubble classes (.mnemo-bubble, .mnemo-row, .msg-enter,
   .mnemo-typing-dot) so the voice feels identical the moment the reveal hands off into #mnemo;
   only the page chrome and per-step input controls are new here. ── */
.intro-page {
  /* Cat 10a (intro F3): dvh fallback, same precedent as .gm-hero-layout, so iOS Safari's toolbar
     never leaves .intro-controls sitting under it before the dvh unit is recalculated. */
  height: 100vh; height: 100dvh; display: flex; flex-direction: column; max-width: 640px; margin: 0 auto;
}
.intro-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px; border-bottom: 1px solid var(--color-border);
  /* Cat 19 (browser W4): edge-to-edge safe-area clearance, only meaningful once the native shell
     ships with viewport-fit=cover; inert (0px) on the web. */
  padding-top: calc(16px + env(safe-area-inset-top, 0px));
}
.intro-stage {
  flex: 1 1 auto; overflow-y: auto; padding: 20px 20px 8px; display: flex; flex-direction: column; gap: 2px;
}
.intro-typing-bubble { display: flex; gap: 4px; padding: 11px 13px; }
/* padding-bottom folds in --kb-inset (Slice H soft-keyboard signal, see the token comment at the
   top of this file) on top of the existing 22px resting clearance, so the controls track clear
   of the on-screen keyboard the same way #mainContainer.is-mnemo does. 0 at rest / desktop. */
.intro-controls {
  padding: 14px 20px calc(22px + var(--kb-inset, 0px));
  border-top: 1px solid var(--color-border); background: var(--color-surface);
}
.intro-form { display: flex; flex-direction: column; gap: 10px; }
.intro-form-row { flex-direction: row; align-items: center; flex-wrap: wrap; }
.intro-form-row .input { flex: 1; min-width: 140px; }
.intro-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.intro-chip { cursor: pointer; border: none; }
.intro-chip:hover { background: color-mix(in srgb, var(--color-primary) 14%, var(--color-muted)); }
/* Cat 12c (intro F2): inherits .chip's small padding with no phone-width rule of its own, unlike
   .toggle-pill. */
@media (max-width: 767px) {
  .intro-chip { min-height: 44px; padding: 8px 14px; }
}

.intro-people-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.intro-person-chip {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  background: var(--color-muted); border-radius: var(--radius); padding: 8px 10px; font-size: var(--font-size-small);
}
/* Cat 10c + Cat 4 (intro F1): the shared .chip-remove is an 18x14px tap area here, and (being a
   non-flex button) the inline SVG baseline-aligns a few px high. A 44x44 hit area from the
   pseudo-element keeps the visible glyph's size and centers it, without touching every other
   .chip-remove user (space.js's identity chips, etc.). */
.intro-person-chip .chip-remove {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
}
.intro-person-chip .chip-remove::before { content: ''; position: absolute; inset: -8px; }

@media (max-width: 560px) {
  .intro-page { max-width: none; }
  .intro-header, .intro-stage, .intro-controls { padding-left: 14px; padding-right: 14px; }
}

/* ── Mnemo micro-animation pass (/orion_disney): transform/opacity only, motion tokens above,
   prefers-reduced-motion already covered globally (the block near the top of this file). ── */

.mnemo-chat { position: relative; } /* anchors the loop-closed celebration badge below */

/* Message entrance: applied via JS only to a bubble that is genuinely new this render (never on
   the initial thread load, never re-applied to a message already on screen). */
.mnemo-row.msg-enter { animation: mnemoMsgEnter var(--dur-base) var(--ease-out) forwards; }
@keyframes mnemoMsgEnter {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Receipt chip: a small state change (an answer just gained a visible payoff), scale-in once. */
.mnemo-receipt { animation: mnemoDotEnter var(--dur-base) var(--ease-spring) forwards; }
.mnemo-receipt-detail { animation: mnemoMsgEnter var(--dur-fast) var(--ease-out) forwards; }
@keyframes mnemoDotEnter {
  from { transform: scale(0); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* Ripple card stagger: JS sets --stagger (0 to 8) per card and adds .stagger-in only on the
   rail's first successful load; subsequent refreshes render instantly (Principle 9's "only on
   first load" rule). */
.mnemo-ripple-card.stagger-in {
  animation: mnemoCardEnter var(--dur-base) var(--ease-out) both;
  animation-delay: calc(var(--stagger, 0) * 40ms);
}
@keyframes mnemoCardEnter {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Stat strip counters (Living Data): a brief bump whenever a 7-day figure actually changes. */
.stat-chip-value.counter-bump { animation: mnemoCounterBump var(--dur-base) var(--ease-spring); }
@keyframes mnemoCounterBump {
  0% { transform: scale(1); }
  40% { transform: scale(1.2); }
  100% { transform: scale(1); }
}

/* The "absorb" pulse: after sending, the composer briefly acknowledges receipt (a soft ring that
   fades), instead of the message just vanishing from the input with no feedback. */
.mnemo-composer.is-absorbing { animation: mnemoAbsorb var(--dur-slow) var(--ease-out); }
@keyframes mnemoAbsorb {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 35%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Sending state: the send button gives quiet, immediate feedback rather than waiting on the
   network round trip to show anything happened. */
.mnemo-send.is-sending { opacity: 0.75; transform: scale(0.97); }

/* The ONE celebratory beat: a soft checkmark pop, centered over the chat, when an answer closes
   an open loop. Every other state change in this page stays in the 150 to 300ms range; this is
   the deliberate exception (Principle 7, reserved for a moment that matters). */
.mnemo-loop-closed-badge {
  position: absolute; left: 50%; top: 46%; z-index: 5; pointer-events: none;
  width: 56px; height: 56px; border-radius: 50%;
  background: color-mix(in srgb, var(--color-success) 18%, var(--color-surface));
  color: var(--color-success); display: flex; align-items: center; justify-content: center;
  font-size: 1.6rem; font-weight: 600; box-shadow: var(--shadow-md);
  animation: mnemoLoopClosedPop 650ms var(--ease-spring) forwards;
}
@keyframes mnemoLoopClosedPop {
  0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
  25% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
  70% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

/* ── Toast notifications: a fixed singleton stack, bottom-center, docked above the bottom nav
   (using the same --bottom-nav-total-h token the page-scroll routes reserve for the bar itself,
   plus a small gap). Reduced-motion is already covered by the global block near the top of this
   file (the * selector zeroes every animation-duration). ── */
.gm-toast-container {
  position: fixed; left: 50%; bottom: calc(var(--bottom-nav-total-h) + 12px); transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 8px; align-items: stretch;
  width: min(92vw, 420px); z-index: 900; pointer-events: none;
}
.gm-toast {
  pointer-events: auto;
  background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
  padding: 10px 14px; font-size: var(--font-size-small); color: var(--color-foreground);
  box-shadow: var(--shadow-md);
  border-left: 3px solid var(--color-border);
  animation: gmToastIn var(--dur-base) var(--ease-out) both;
}
@keyframes gmToastIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.gm-toast-error { border-left-color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface)); }
.gm-toast-success { border-left-color: var(--color-success); background: color-mix(in srgb, var(--color-success) 8%, var(--color-surface)); }
.gm-toast-info { border-left-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)); }

/* ── Confirm dialog: a centered card over a translucent scrim, replacing the browser's native
   confirm(). The scrim is derived from --color-background (not a hardcoded black) so it reads
   correctly whether the page is currently light or dark. ── */
.gm-confirm-overlay {
  position: fixed; inset: 0; z-index: 950;
  background: color-mix(in srgb, var(--color-background) 55%, transparent);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  animation: gmConfirmOverlayIn var(--dur-fast) var(--ease-out) both;
}
@keyframes gmConfirmOverlayIn { from { opacity: 0; } to { opacity: 1; } }
.gm-confirm-sheet {
  background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
  padding: 20px; max-width: 360px; width: 100%; box-shadow: var(--shadow-lg);
  animation: gmConfirmSheetIn var(--dur-base) var(--ease-out) both;
}
@keyframes gmConfirmSheetIn {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
.gm-confirm-msg { margin: 0 0 16px; font-size: var(--font-size-body); color: var(--color-foreground); }
.gm-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }

/* ══ Question intelligence (docs/plans/2026-09-05-question-intelligence.md) ═════════════════════
   New classes only, appended as one block; no existing rule above is edited. Everything here
   references the :root tokens (never a raw hex or a raw ms), so light and dark both follow the
   palette with no second definition. Surfaces: the question card (question-chips.js) rendered
   inside the Journal, Mnemo and the Admin page, plus the People page rule track record line. */

.qi-card {
  border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
  background: color-mix(in srgb, var(--color-primary) 5%, var(--color-surface));
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-top: 12px;
  outline: none;
}
.qi-card:focus-visible { box-shadow: 0 0 0 2px var(--color-primary); }
.qi-card.is-busy { opacity: 0.6; }
.qi-card.is-answered {
  border-color: color-mix(in srgb, var(--color-success) 40%, var(--color-border));
  background: color-mix(in srgb, var(--color-success) 6%, var(--color-surface));
}

.qi-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.qi-badge {
  display: inline-block; padding: 3px 8px 2px; border-radius: 999px;
  font-size: var(--font-size-small);
  background: color-mix(in srgb, var(--color-primary) 16%, transparent); color: var(--color-primary);
}
.qi-from { font-size: var(--font-size-small); color: var(--color-muted-fg); }
.qi-reason { margin: 8px 0 0; font-size: var(--font-size-small); color: var(--color-muted-fg); }
.qi-text { margin: 6px 0 0; font-size: var(--font-size-body); }

.qi-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.qi-chip {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-foreground); font-size: var(--font-size-small); font-family: inherit;
  cursor: pointer; transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.qi-chip:hover:not(:disabled) { background: var(--color-muted); border-color: var(--color-primary); }
.qi-chip:disabled { cursor: default; opacity: 0.5; }
.qi-chip-num {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; border-radius: 4px;
  background: var(--color-muted); color: var(--color-muted-fg);
  font-size: var(--font-size-label); line-height: 1;
}
.qi-chip-quiet { color: var(--color-muted-fg); }

.qi-panel { margin-top: 10px; }
.qi-text-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.qi-text-row .qi-input { flex: 1 1 200px; min-width: 0; }
.qi-person-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.qi-person {
  min-height: 32px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-foreground); font-size: var(--font-size-small); font-family: inherit; cursor: pointer;
}
.qi-person:hover { border-color: var(--color-primary); background: var(--color-muted); }

.qi-foot { margin: 10px 0 0; }
.qi-suppress {
  border: none; background: none; padding: 0; cursor: pointer;
  color: var(--color-muted-fg); font-size: var(--font-size-small); font-family: inherit;
  text-decoration: underline; text-underline-offset: 3px;
}
.qi-suppress:hover { color: var(--color-foreground); }
.qi-status { margin: 8px 0 0; font-size: var(--font-size-small); color: var(--color-success); }

/* The contradiction decide row: three chips on a card the engine flagged NEEDS REVIEW. Reuses
   .qi-chip so a decide chip and an answer chip are visibly the same affordance. */
.qi-verdict { margin-top: 10px; }
.qi-verdict-label { font-size: var(--font-size-small); color: var(--color-muted-fg); margin: 0 0 6px; }

/* People page rule track record (merge intelligence L2, docs/merge-intelligence.md Surfaces):
   one quiet line under a proposal's evidence or a merged row. */
.rule-record { font-size: var(--font-size-small); color: var(--color-muted-fg); margin-top: 6px; }
.rule-record .rule-record-key { color: var(--color-foreground); }

/* Link WhatsApp card (owner request 21/09/26, web/js/link-whatsapp.js): the self-serve pairing
   card offered both above the tabs (a coach's own row) and inside the Coaches tab's invite
   result (a Lead Coach handing the phone to a new coach). Holds at 360px: rows wrap to a
   stacked layout and the copy button goes full-width instead of squeezing beside a long value. */
.link-wa-card .link-wa-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; margin-top: 10px;
}
.link-wa-field { min-width: 0; }
.link-wa-value, .link-wa-code { overflow-wrap: anywhere; }
.link-wa-code { font-size: 1.4rem; letter-spacing: 0.08em; }
.link-wa-qr-wrap { margin-top: 16px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.link-wa-qr { display: flex; justify-content: center; min-height: 40px; }
.link-wa-qr-canvas { max-width: 220px; width: 100%; height: auto; border-radius: 8px; }
.link-wa-open { max-width: 280px; }
.link-wa-expiry { margin-top: 12px; }
.link-wa-new-code { margin-top: 10px; }
@media (max-width: 360px) {
  .link-wa-card .link-wa-row { flex-direction: column; align-items: stretch; }
  .link-wa-card .link-wa-row .btn { width: 100%; }
}
