/* Shared tokens, nav, and buttons across all pages. Colors are pulled
   straight from the iOS app's CardieTheme.swift so the website actually
   matches the app's real brand instead of a separate invented palette. */
/* Bright/colorful palette is the default look. "Dark Mode" (toggled via
   the nav button on every page, persisted in localStorage) swaps to the
   values below. Same token names either way, so every component restyles
   for free. */
:root {
  color-scheme: light;
  --bg: #f4f7fb;
  --card: #e3ecf7;
  --card-elevated: #d3e2f3;
  --ink: #1b2536;
  --ink-dim: rgba(27, 37, 54, 0.65);
  --accent: #4d7fe0;
  --teal: #14b8a6;
  --honey: #ff9f1c;
  --honey-light: #ffc861;
  --honey-ink: #3a1f00;
  --honey-pale: #fff6d8;
  --border: rgba(27, 37, 54, 0.14);
  /* Primary-button surface — follows --accent (so it matches whichever
     theme family is chosen: blue by default, lilac/pink/gray/mono if the
     student picked one) rather than a fixed color. --accent-ink is the
     text color that stays readable on top of it — most families keep
     white here, but a family whose --accent goes pale in dark mode (see
     lilac/pink/gray/mono below) overrides it to a dark ink instead.
     --btn-primary-border stays a fixed dark neutral in every family/theme
     on purpose — it's the button shape's own outline, not a themed color,
     the same way the reference button's border doesn't change with fill
     color. --honey is untouched — it stays the Honey currency's own color
     (coin icon, milestone modal, wardrobe) and now also marks "special/
     featured" buttons (.btn-feature) specifically, separate from the
     primary-action blue. */
  --accent-ink: #ffffff;
  --btn-primary-border: #142433;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #10151f;
  --card: #1a2230;
  --card-elevated: #222c3e;
  --ink: #eef2f8;
  --ink-dim: rgba(238, 242, 248, 0.62);
  --accent: #7fa8ff;
  --accent-ink: #ffffff;
  --teal: #6bb3ad;
  --honey: #f2bf59;
  --honey-light: #ffd27a;
  --honey-ink: #2b1c05;
  --honey-pale: #fff6d8;
  --border: rgba(255, 255, 255, 0.1);
}

/* --- Theme families (Appearance card, dashboard) --- */
/* Blue is the bare :root above/its dark variant above — the default look
   new users start on. Each family below only overrides the tokens that
   actually vary by family (background/card/ink/accent/border) — honey and
   teal stay constant across every family. */
:root[data-family="lilac"] {
  --bg: #fdf6ff;
  --card: #f3e4ff;
  --card-elevated: #ead2ff;
  --ink: #2b1b3d;
  --ink-dim: rgba(43, 27, 61, 0.65);
  --accent: #b34bff;
  --accent-ink: #ffffff;
  --border: rgba(43, 27, 61, 0.14);
}
:root[data-family="lilac"][data-theme="dark"] {
  --bg: #121214;
  --card: #1c1c22;
  --card-elevated: #24242b;
  --ink: #f5f3ee;
  --ink-dim: rgba(245, 243, 238, 0.62);
  --accent: #d9b8f0;
  --accent-ink: #2b1b3d;
  --border: rgba(255, 255, 255, 0.1);
}

:root[data-family="pink"] {
  --bg: #fff5f7;
  --card: #ffe3ea;
  --card-elevated: #ffd2dd;
  --ink: #3d1b26;
  --ink-dim: rgba(61, 27, 38, 0.65);
  --accent: #e0447e;
  --accent-ink: #ffffff;
  --border: rgba(61, 27, 38, 0.14);
}
:root[data-family="pink"][data-theme="dark"] {
  --bg: #1a1216;
  --card: #241a1f;
  --card-elevated: #2e2129;
  --ink: #f8ecef;
  --ink-dim: rgba(248, 236, 239, 0.62);
  --accent: #ff8fb3;
  --accent-ink: #3d1b26;
  --border: rgba(255, 255, 255, 0.1);
}

:root[data-family="gray"] {
  --bg: #f7f7f8;
  --card: #eceeef;
  --card-elevated: #e2e4e6;
  --ink: #202224;
  --ink-dim: rgba(32, 34, 36, 0.65);
  --accent: #5b6168;
  --accent-ink: #ffffff;
  --border: rgba(32, 34, 36, 0.14);
}
:root[data-family="gray"][data-theme="dark"] {
  --bg: #131415;
  --card: #1c1d1f;
  --card-elevated: #242628;
  --ink: #f0f1f2;
  --ink-dim: rgba(240, 241, 242, 0.62);
  --accent: #a7acb3;
  --accent-ink: #202224;
  --border: rgba(255, 255, 255, 0.1);
}

/* Low Stim — pure black/white, no color tint anywhere, including honey
   (its counter is hidden outside the dashboard entirely — see each
   page's applyThemeFamily). */
:root[data-family="mono"] {
  --bg: #f4f4f4;
  --card: #f0f0f0;
  --card-elevated: #e2e2e2;
  --ink: #000000;
  --ink-dim: rgba(0, 0, 0, 0.6);
  --accent: #000000;
  --teal: #000000;
  --honey: #000000;
  --honey-light: #333333;
  --honey-ink: #ffffff;
  --honey-pale: #e2e2e2;
  --border: rgba(0, 0, 0, 0.16);
}
:root[data-family="mono"][data-theme="dark"] {
  --bg: #000000;
  --card: #141414;
  --card-elevated: #202020;
  --ink: #ffffff;
  --ink-dim: rgba(255, 255, 255, 0.62);
  --accent: #ffffff;
  --teal: #ffffff;
  --honey: #ffffff;
  --honey-light: #cccccc;
  --honey-ink: #000000;
  --honey-pale: #202020;
  --border: rgba(255, 255, 255, 0.16);
}
* { box-sizing: border-box; }
/* Every page's real content starts hidden behind an auth/data check and
   pops in a beat later (see each page's #app-root, main, etc.) — going
   from "just a short nav bar" to a full page can easily cross the
   threshold where a vertical scrollbar becomes necessary. On a system
   that reserves real layout space for scrollbars (the common case
   outside macOS's default overlay-scrollbar setting), that scrollbar
   appearing shrinks the available width, and since .site-nav is centered
   inside body, EVERYTHING in the header shifts sideways at that exact
   moment — a second, independent cause of the same "header jumps on
   load" bug alongside the nav-pending fix elsewhere in this file.
   Reserving the gutter unconditionally means the width never changes,
   whether or not a scrollbar ends up actually needed. */
html { scrollbar-gutter: stable; }
html, body { margin: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}
a { color: inherit; }

/* --- Nav --- */
.site-nav {
  width: 100%;
  max-width: 1040px;
  display: flex;
  align-items: center;
  /* NOT justify-content: space-between — with hamburger-nav.js inserting
     a 4th item (the menu button) before .brand, space-between's gap
     redistribution meant brand's own position depended on every other
     item's width, including nav-right's (0 while pending, real width once
     signed-in/out resolves) — the logo visibly slid sideways once nav-
     right's content landed. flex-start makes every item's position
     depend only on what's strictly to its left; .nav-corner below is
     what actually gets pinned to the right edge, via margin-left: auto,
     which doesn't have that same all-items-depend-on-each-other problem. */
  justify-content: flex-start;
  gap: 16px;
  padding: 22px 24px 0;
}
.site-nav .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  flex-shrink: 0;
}
.site-nav .brand img {
  height: 30px;
  width: auto;
  display: block;
  /* The logo art is white-on-transparent, invisible on the default light
     background — flip it dark instead of needing a second logo asset. */
  filter: invert(1);
}
:root[data-theme="dark"] .site-nav .brand img {
  filter: none;
}
.site-nav .nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  flex-wrap: nowrap;
  min-width: 0;
  overflow-x: auto;
  /* No margin-left: auto here (moved to .nav-corner — see its own comment)
     — nav-right sits at a fixed gap right after .brand instead, so its own
     position never depends on .nav-corner's width. That used to not be
     true: with the auto margin on nav-right, .nav-corner's width (the
     Honey pill's digit count changing once the real lifetime total loads,
     or the Cardie Plus pill popping in) fed back into how much margin
     nav-right got, which visibly nudged the ALREADY-VISIBLE nav-right
     links sideways every time something later in nav-corner changed size. */
}
/* Every nav element whose real visibility depends on an async check
   (signed-in state, subscription status, theme prefs) starts with this
   class instead of `hidden`/`display:none` — it still lays out normally
   (just invisible), so its box is already reserved at first paint. The
   check removes this class once it knows the real answer, revealing the
   element in place with no resize or reflow of anything else in the nav.
   Using `hidden`/`display:none` here instead was the actual cause of the
   header visibly jumping on every page load: those remove the element
   from layout entirely, so the whole nav row reflows around it the
   moment JS reveals it a beat later. A confirmed PERMANENT hide (a
   subscriber never needs the Cardie Plus pill again, mono theme never
   shows the Honey pill) should still use real hidden/display:none, since
   that's a stable end state, not something pending. */
.nav-pending { visibility: hidden !important; pointer-events: none; }
.site-nav .nav-link {
  color: var(--ink-dim);
  text-decoration: none;
  padding: 8px 6px;
  font-weight: 600;
  font-size: 14px;
  white-space: nowrap;
  flex-shrink: 0;
}
.site-nav .nav-link:hover { color: var(--ink); }
@media (max-width: 480px) {
  .site-nav { gap: 8px; padding: 16px 16px 0; }
  .site-nav .nav-right { gap: 6px; font-size: 13px; }
  .site-nav .nav-corner { gap: 6px; }
  .site-nav .nav-link { font-size: 13px; padding: 6px 4px; }
  .site-nav .pill, .site-nav .btn-ghost { font-size: 12px; padding: 6px 10px; white-space: nowrap; flex-shrink: 0; }
  /* Too narrow to fit the full bar legibly — keep only the three things
     that matter most (Dashboard, Cardie Plus, the Dark/Light toggle) and
     drop the rest. Everything hidden here (Dump Notes, My Classes,
     Friends, Sign out, the Honey pill) is still reachable from inside the
     Dashboard itself. */
  .site-nav .nav-link,
  .site-nav #sign-out-btn,
  .site-nav .honey-pill {
    display: none !important;
  }
}

/* --- Buttons --- */
/* ---- Button hierarchy ----
   PRIMARY (.btn-primary) — the one obvious next action on a screen: solid
     --accent fill, pill-shaped, a light "chunky" edge (thin border + a
     short flat shadow it presses into on click) for Cardie's playful
     character without the button visually shouting.
   SECONDARY (.btn-outline) — an important alternative action, quieter:
     bordered, no fill, no shadow — present but doesn't compete.
   TERTIARY (.btn-ghost, .linklike) — low-priority actions, closest to
     invisible until interacted with (a text link, or a faint outline).
   SPECIAL/FEATURE (.btn-feature) — a featured experience worth calling
     out as distinct from the everyday primary action (Cardie Blitz, and
     future ones like it) — same shape language as primary, but in Honey's
     gold rather than --accent, so it reads as "something special" without
     inventing a whole new visual language.
   All four share one shape/weight vocabulary (rounded, bold type, a small
   press-in interaction on the two filled tiers) so they read as one family
   even though only one of them should ever be the loudest thing on screen. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 14px;
  font-family: inherit;
  font-weight: 700;
  font-size: 15px;
  padding: 12px 20px;
  border: none;
  cursor: pointer;
  text-decoration: none;
  gap: 8px;
  line-height: 1;
}
/* Without this, .btn's own unconditional `display: inline-flex` above
   beats the browser's default [hidden]{display:none} (author styles
   always win over UA styles at equal specificity) — any .btn-classed
   element toggled via the `hidden` attribute/property would silently stay
   visible. Scoped here once so every .btn everywhere is safe to hide this
   way, rather than needing a one-off override wherever it's used. */
.btn[hidden] { display: none; }
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border: 2px solid var(--btn-primary-border);
  border-radius: 999px;
  box-shadow: 0 3px 0 var(--btn-primary-border);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
/* Presses in toward its own shadow on hover/click rather than just
   brightening — hover eases halfway in, active/click bottoms out flush
   with no shadow left, like a real button being pushed down. Much
   shorter throw than a diagnostic first pass at this (was a 4px diagonal
   drop-shadow + 3px border) — same idea, far less visual weight. */
.btn-primary:not(:disabled):hover {
  transform: translateY(1px);
  box-shadow: 0 2px 0 var(--btn-primary-border);
}
.btn-primary:not(:disabled):active {
  transform: translateY(3px);
  box-shadow: 0 0 0 var(--btn-primary-border);
}
.btn-primary:disabled { opacity: 0.45; cursor: default; box-shadow: none; }
.btn-feature {
  background: linear-gradient(135deg, var(--honey-light), var(--honey));
  color: var(--honey-ink);
  border: 2px solid var(--honey-ink);
  border-radius: 999px;
  box-shadow: 0 3px 0 var(--honey-ink);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.btn-feature:not(:disabled):hover {
  transform: translateY(1px);
  box-shadow: 0 2px 0 var(--honey-ink);
}
.btn-feature:not(:disabled):active {
  transform: translateY(3px);
  box-shadow: 0 0 0 var(--honey-ink);
}
.btn-feature:disabled { opacity: 0.45; cursor: default; box-shadow: none; }
.btn-outline {
  background: rgba(255, 255, 255, 0.05);
  color: var(--ink);
  border: 1px solid var(--border);
}
.btn-outline:hover { border-color: var(--accent); }
.btn-outline.danger { color: #ff9b9b; border-color: rgba(255, 155, 155, 0.4); }
.btn-outline.danger:hover { border-color: #ff9b9b; background: rgba(255, 155, 155, 0.08); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-dim);
  background: rgba(255, 255, 255, 0.03);
  text-decoration: none;
  white-space: nowrap;
}
.pill:hover { border-color: var(--accent); color: var(--ink); }
/* Same [hidden]-defeated-by-author-display issue as .btn above — the
   Cardie Plus nav pill (.pill) is toggled via the `hidden` attribute and
   needs this to actually disappear rather than silently staying visible. */
.pill[hidden] { display: none; }

/* ---- Hamburger menu (see hamburger-nav.js) ---- */
.hamburger-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--ink-dim);
  cursor: pointer;
  padding: 0;
}
.hamburger-btn:hover, .hamburger-btn:focus-visible { color: var(--ink); border-color: var(--accent); outline: none; }
/* Same [hidden]-defeated-by-author-display issue as .btn/.pill above —
   the button is hidden until sign-in resolves and needs this to actually
   disappear rather than silently staying visible. */
.hamburger-btn[hidden] { display: none; }

.guest-theme-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  color: var(--ink-dim);
  cursor: pointer;
  padding: 0;
}
.guest-theme-toggle-btn:hover, .guest-theme-toggle-btn:focus-visible { color: var(--ink); border-color: var(--accent); outline: none; }
/* Same [hidden]-defeated-by-author-display issue as every other nav
   control above — hidden until auth state resolves, and hidden again for
   good if it turns out to be a real signed-in account. */
.guest-theme-toggle-btn[hidden] { display: none; }

.hamburger-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 880;
}
.hamburger-overlay[hidden] { display: none; }

.hamburger-drawer {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 881;
  width: 280px;
  max-width: calc(100vw - 40px);
  background: var(--card);
  border-right: 1px solid var(--border);
  box-shadow: 20px 0 48px -20px rgba(0, 0, 0, 0.4);
  padding: 20px;
  transform: translateX(-100%);
  transition: transform 0.2s ease;
  display: flex;
  flex-direction: column;
}
.hamburger-drawer.open { transform: translateX(0); }
.hamburger-drawer[hidden] { display: none; }
.hamburger-head {
  position: relative;
  display: flex;
  align-items: center;
  margin-bottom: 20px;
  padding-right: 28px;
}
.hamburger-brand { font-size: 18px; font-weight: 700; color: var(--ink); }
.hamburger-links { display: flex; flex-direction: column; gap: 2px; margin-bottom: 12px; }
.hamburger-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: 10px;
  color: var(--ink);
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  background: none;
  border: none;
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
}
.hamburger-link:hover, .hamburger-link:focus-visible { background: rgba(127, 127, 127, 0.12); outline: none; }
.hamburger-link[aria-current="page"] { color: var(--accent); background: rgba(77, 127, 224, 0.1); }
.hamburger-link svg { flex-shrink: 0; color: var(--ink-dim); }
.hamburger-link[aria-current="page"] svg { color: var(--accent); }
.hamburger-signout {
  margin-top: auto;
  border-top: 1px solid var(--border);
  padding-top: 16px;
  color: var(--ink-dim);
}

.btn-ghost {
  background: none;
  color: var(--ink-dim);
  border: 1px solid var(--border);
  font-family: inherit;
  font-weight: 600;
  font-size: 13px;
  padding: 8px 14px;
  border-radius: 10px;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.btn-ghost:hover { color: var(--ink); border-color: var(--accent); }

.honey-pill {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-weight: 700;
  color: var(--honey-ink);
  background: var(--honey-pale);
  /* A subtle outline, same weight as every other pill on the site (see
     .pill above) — previously none at all, so the honey counter was the
     one pill that just floated as a flat block of color with no edge. */
  border: 1px solid var(--border);
}
/* min-width reserves room for a real total (not just the "0" every page
   starts with) so the pill's rendered width — and therefore the position
   of whatever sits next to it (Help, Cardie Plus) — doesn't change the
   moment the real lifetime total loads in. Without this, visibility:hidden
   (see .nav-pending) only stops the pill itself from popping into view;
   its box still resizes to fit new text while still invisible, which
   pushes already-visible siblings sideways regardless. display:inline-block
   is needed for min-width to apply at all — a bare inline span ignores it. */
.honey-pill .honey-amount { display: inline-block; min-width: 3ch; font-weight: 700; transition: transform 0.2s ease; }
.honey-pill.bump .honey-amount { transform: scale(1.2); }
.honey-emoji { font-size: 15px; line-height: 1; }
.honey-chevron { opacity: 0.45; font-weight: 700; margin-left: -1px; }

/* Top-right corner group: honey pill sits just left of the Light/Dark
   toggle, both pinned to the far right of the nav regardless of how
   long the middle nav-right group (links, account pill) grows. */
.nav-corner {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  /* Pinned to the far right by absorbing all remaining free space itself,
     so any width change inside nav-corner (Honey total's digit count
     landing, Cardie Plus pill revealing) only changes nav-corner's own
     margin — never nav-right's — and can't shift the already-visible nav
     links to the left. See .nav-right's comment for the bug this replaced. */
  margin-left: auto;
}

/* --- Shared icon set (see icons.mjs) --- */
.icon-svg {
  flex-shrink: 0;
  color: currentColor;
  vertical-align: -3px;
}
.icon-btn-svg {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--ink-dim);
  cursor: pointer;
  padding: 4px;
  border-radius: 6px;
}
.icon-btn-svg:hover { color: var(--ink); }
.icon-btn-svg.danger:hover { color: #ff9b9b; }

/* Gentle, quiet motion for milestones/sprint-complete — replaces emoji
   "celebration" with a soft fade+rise instead of confetti/fireworks. */
@keyframes gentle-rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.gentle-in { animation: gentle-rise 0.4s ease both; }

/* --- Footer --- */
.site-footer {
  width: 100%;
  max-width: 1040px;
  margin-top: auto;
  padding: 48px 24px 44px;
  text-align: center;
  font-size: 13px;
  color: var(--ink-dim);
}
.site-footer a {
  color: var(--ink-dim);
  text-decoration: underline;
}
.site-footer a:hover { color: var(--ink); }
.site-footer .footer-links { margin-top: 6px; }
.site-footer .footer-contact { margin-top: 4px; font-size: 12px; }

/* ===================================================================
   Accessibility engine — driven by attributes on <html>, set from the
   dashboard's Accessibility panel and persisted per-account (see
   fetchAccessibilityPrefs/saveAccessibilityPref in firebase-init.js).
   Each control here is a real, independent opt-in; none of them are
   bundled together, since different people need different combinations
   (a low-vision reader wants larger text but not necessarily reduced
   motion; someone with vestibular sensitivity wants reduced motion but
   not larger text, etc).
   =================================================================== */

/* --- Reduce motion (WCAG 2.3.3) --- the standard recipe: collapse every
   animation/transition to effectively instant rather than removing them
   outright, so state changes (a flipped card, a closed panel) still
   land in the right end state without the motion itself. Respects the
   OS-level prefers-reduced-motion signal as the default even before a
   signed-in preference loads (see each page's inline head script). */
:root[data-reduce-motion="true"] *,
:root[data-reduce-motion="true"] *::before,
:root[data-reduce-motion="true"] *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}

/* --- Larger text --- scales the whole page, the same way native browser
   zoom does, rather than only the elements that happen to use rem units
   (this site's font-sizes are mostly fixed px, inherited from the iOS
   app's own type scale, so a root-font-size trick alone wouldn't reach
   most text). */
:root[data-font-size="large"] body { zoom: 1.15; }
:root[data-font-size="xlarge"] body { zoom: 1.3; }

/* --- Text spacing (WCAG 1.4.12 minimums) --- line/letter/word spacing
   floors that measurably help dyslexic and low-vision readers track a
   line of text. */
:root[data-text-spacing="true"] body {
  line-height: 1.5 !important;
}
:root[data-text-spacing="true"] p,
:root[data-text-spacing="true"] .content,
:root[data-text-spacing="true"] li {
  letter-spacing: 0.12em !important;
  word-spacing: 0.16em !important;
}

/* --- Dyslexia-friendly font --- Atkinson Hyperlegible (Braille
   Institute): designed and tested for letterform distinction rather than
   a decorative "dyslexia font," with research behind its legibility
   gains for a broader group of readers, not just dyslexic ones. Loaded
   only when this attribute is present (see each page's <head>). */
:root[data-dyslexia-font="true"] body {
  font-family: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
:root[data-dyslexia-font="true"] input,
:root[data-dyslexia-font="true"] textarea,
:root[data-dyslexia-font="true"] button,
:root[data-dyslexia-font="true"] select {
  font-family: inherit;
}

/* --- Hide the fidget toy --- opt-out for anyone who finds it more
   distracting than regulating (see /dump's fidget-toy and fidget-hint). */
:root[data-hide-fidget="true"] .fidget-toy,
:root[data-hide-fidget="true"] .fidget-hint {
  display: none !important;
}

/* ===================================================================
   Cardie Plus nav button (assets/cardie-plus-nav.js) — sits in every
   signed-in page's nav-corner, next to the Dark/Light toggle, but only
   for accounts that don't already have Cardie Plus (a subscriber never
   sees this — same "no persistent nagging" spirit as the paywall
   itself, just phrased as an invitation instead of a wall). Starts
   `hidden` and is only revealed once the subscription check actually
   confirms someone doesn't have it, so signed-out visitors and
   subscribers never see so much as a flash of it.
   =================================================================== */
.cardie-plus-nav { position: relative; }
.cardie-plus-nav-btn {
  background: var(--honey-pale);
  color: var(--honey-ink);
  border: 1px solid rgba(242, 191, 89, 0.55);
}
.cardie-plus-nav-btn:hover { filter: brightness(0.97); }

.cardie-plus-nav-panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 60;
  width: 320px;
  max-width: calc(100vw - 32px);
  background: var(--card-elevated);
  border-radius: 18px;
  padding: 20px;
  box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.45);
}
.cardie-plus-nav-panel[hidden] { display: none; }
.cardie-plus-nav-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}
.cardie-plus-nav-head h2 { margin: 0; font-size: 17px; }
.cardie-plus-nav-panel p {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--ink-dim);
  margin: 0 0 12px;
}
.cardie-plus-nav-panel p strong { color: var(--ink); }
.cardie-plus-nav-price { font-size: 14px; color: var(--ink); margin: 0 0 16px; }
.cardie-plus-nav-get-btn { width: 100%; justify-content: center; }

/* ===================================================================
   Feedback widget (assets/feedback-widget.js) — floating "Feedback"
   tab, present on every page. Pure token reuse, no new colors: the
   trigger is a .pill-style surface, the submit button is .btn-primary,
   chips borrow the same aria-pressed/accent pattern used elsewhere on
   the site (see dashboard's Appearance toggle). Respects the
   accessibility engine above for free — reduce-motion already targets
   `*`, and dyslexia-font/text-spacing apply automatically since nothing
   here overrides font-family or reaches for px sizing outside the same
   scale the rest of the site uses.
   =================================================================== */
.feedback-widget .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.feedback-fab {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 900;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 10px 18px;
  font: inherit;
  font-weight: 700;
  font-size: 13px;
  color: var(--ink);
  background: var(--card);
  box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}
.feedback-fab:hover { border-color: var(--accent); }
/* Same [hidden]-defeated-by-author-display issue as .btn/.pill above —
   without this, the Feedback button (hidden by default until a signed-in
   check passes) would show for signed-out visitors too. */
.feedback-fab[hidden] { display: none; }

.feedback-panel {
  position: fixed;
  right: 20px;
  bottom: 76px;
  z-index: 901;
  width: 320px;
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 116px);
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 18px;
  box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.45);
}
.feedback-panel[hidden] { display: none; }

.feedback-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 14px;
}
.feedback-panel-head h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.3;
}

.feedback-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.feedback-chip {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 6px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
}
.feedback-chip:hover { border-color: var(--accent); color: var(--ink); }
.feedback-chip[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

.feedback-field { margin-bottom: 10px; }
.feedback-field textarea,
.feedback-field input {
  width: 100%;
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  resize: vertical;
}
.feedback-field textarea:focus,
.feedback-field input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.feedback-error {
  margin: 0 0 10px;
  font-size: 13px;
  color: #e0447e;
}
.feedback-error[hidden] { display: none; }

.feedback-actions { display: flex; justify-content: flex-end; }
.feedback-submit { width: 100%; justify-content: center; }

.feedback-survey-note {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 13px;
  color: var(--ink-dim);
  text-align: center;
}
.feedback-survey-note a { color: var(--ink); font-weight: 600; }

.feedback-success p {
  margin: 0 0 10px;
  font-size: 14px;
  color: var(--ink-dim);
  line-height: 1.5;
}
.feedback-success[hidden] { display: none; }
.feedback-link {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-dim);
  background: none;
  border: none;
  padding: 0;
  text-decoration: underline;
  cursor: pointer;
}
.feedback-link:hover { color: var(--ink); }

@media (max-width: 480px) {
  .feedback-fab { right: 14px; bottom: 14px; }
  .feedback-panel { right: 14px; left: 14px; width: auto; bottom: 70px; }
}

/* ---- Tooltips ---- */
/* One shared, CSS-only pattern for the whole site — put a short
   data-tooltip="..." on any element and it gets a small dark chip above
   it on hover/keyboard-focus, no JS. Meant for icon-only controls (a
   share icon, a trash icon) where the label isn't already visible as
   text; a button that already reads "Save to My Classes" doesn't need
   one too. Colors deliberately invert var(--ink)/var(--bg) rather than a
   fixed color, so the chip stays high-contrast against the page behind
   it in both light and dark theme without a separate dark-mode override. */
[data-tooltip] {
  position: relative;
}
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  max-width: 220px;
  width: max-content;
  background: var(--ink);
  color: var(--bg);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  text-align: center;
  white-space: normal;
  padding: 6px 10px;
  border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease, transform 0.12s ease;
  transition-delay: 0.05s;
  z-index: 30;
}
/* Small arrow under the chip pointing at whatever's hovered — in a
   tightly stacked list (e.g. the fidget customization rows) the chip
   can end up overlapping the row above it since there's no clear air
   to pop up into; the arrow keeps it obvious which control the chip is
   actually describing even when it visually covers a neighbor. */
[data-tooltip]::before {
  content: "";
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  border: 5px solid transparent;
  border-top-color: var(--ink);
  border-bottom-width: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease, transform 0.12s ease;
  transition-delay: 0.05s;
  z-index: 30;
}
/* Hover-triggered only where a real pointer can hover — on a touch
   device this would otherwise stick open after a tap until something
   else is touched, since there's no mouse to move away and end the
   :hover state. Keyboard focus (below) still works everywhere. */
@media (hover: hover) {
  [data-tooltip]:hover::after,
  [data-tooltip]:hover::before {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}
[data-tooltip]:focus-visible::after,
[data-tooltip]:focus-visible::before {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ---- Guide system (see guide.js) ---- */
/* Replaces the earlier "?" badge on every explainable element. New shape:
   a one-time welcome card (.guide-intro), a one-time soft highlight ring
   for a first-time interaction (.guide-highlight-*), and a small handful
   of click/tap/focus-triggered "i" icons for genuinely unfamiliar controls
   (.guide-hint-*). Nothing here relies on hover, and nothing here dims or
   covers the rest of the screen. */

/* 1. First-use intro — an inline card at the top of <main>, not a modal.
   Same visual language as the rest of the site's cards. */
.guide-intro {
  position: relative;
  background: linear-gradient(135deg, var(--card), var(--card-elevated));
  border-radius: 18px;
  padding: 20px 44px 20px 20px;
  margin-bottom: 20px;
}
.guide-intro-headline { font-size: 17px; font-weight: 700; color: var(--ink); margin: 0 0 4px; }
.guide-intro-body { font-size: 14px; line-height: 1.55; color: var(--ink-dim); margin: 0 0 14px; }
.guide-intro-action { display: inline-flex; }

/* 2. Feature highlight — a soft ring drawn around the live element (not a
   copy of it, not a dimmed backdrop) plus a small floating card with one
   line and one action. */
.guide-highlight-ring {
  position: fixed;
  z-index: 850;
  border-radius: 14px;
  border: 2px solid var(--accent);
  box-shadow: 0 0 0 4px rgba(77, 127, 224, 0.18);
  pointer-events: none;
}
.guide-highlight-card {
  position: fixed;
  z-index: 851;
  max-width: 260px;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px;
  box-shadow: 0 14px 36px -12px rgba(0, 0, 0, 0.45);
}
.guide-highlight-card p { font-size: 13px; line-height: 1.5; color: var(--ink); margin: 0 0 12px; }
.guide-highlight-action { width: 100%; }

/* 3. Sparse hint icon — small, quiet, never hover-only. Reuses the same
   [hidden]-defeated-by-author-display fix as .btn/.pill elsewhere. */
.guide-hint-icon {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  color: var(--ink-dim);
  font-size: 11px;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.guide-hint-icon:hover, .guide-hint-icon:focus-visible, .guide-hint-icon.is-open {
  color: var(--ink);
  border-color: var(--accent);
  outline: none;
}
.guide-hint-callout {
  position: fixed;
  z-index: 852;
  max-width: 240px;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 30px 12px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  box-shadow: 0 14px 36px -12px rgba(0, 0, 0, 0.45);
}
.guide-hint-callout[hidden] { display: none; }

/* Shared small "×" close button used by the intro card, the highlight's
   hint callout, and the hamburger menu drawer. */
.guide-close-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 24px;
  height: 24px;
  background: none;
  border: none;
  color: var(--ink-dim);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  border-radius: 6px;
}
.guide-close-btn:hover, .guide-close-btn:focus-visible { color: var(--ink); outline: none; background: rgba(127, 127, 127, 0.12); }
.guide-hint-callout .guide-close-btn { top: 8px; right: 8px; }

/* ---------- Study guide: spatial map + concept lenses ----------
   Shared by /studyguide and /try (see study-guide-view.mjs) — a circular
   arrangement around Cardie instead of a horizontal row of rectangles.
   Node positions are set inline as percentages of .sgm-map's own box
   (kept square via aspect-ratio), so this holds up at any concept count
   without per-breakpoint pixel tuning. */
.sgm-header {
  margin-bottom: 14px;
  padding: 4px 0 13px;
  border-bottom: 1px solid var(--border);
}
.sgm-header-title-row { display: flex; align-items: center; gap: 8px; }
.sgm-header-topic { font-size: 20px; line-height: 1.35; font-weight: 800; letter-spacing: -0.01em; margin: 0; color: var(--ink); }
/* A pill with a chevron, not a bare circle with just an "i" in it — the
   circle read as a plain info glyph, not something that opens anything.
   The chevron is the one universally-understood "this expands" signal,
   so it's what actually needed adding; everything else (size, weight,
   border) stays as close to the old quiet treatment as possible so this
   doesn't turn into a second loud button next to the topic title. */
.sgm-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  height: 26px;
  padding: 0 9px;
  border-radius: 999px;
  background: none;
  border: 1px solid var(--border);
  color: var(--ink-dim);
  cursor: pointer;
  flex-shrink: 0;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.sgm-info-btn:hover, .sgm-info-btn.active { border-color: var(--accent); color: var(--ink); }
.sgm-info-btn-chevron { transition: transform 0.15s ease; }
.sgm-info-btn.active .sgm-info-btn-chevron { transform: rotate(180deg); }
.sgm-header-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; flex-wrap: wrap; }
.sgm-header-progress-group { display: flex; align-items: center; gap: 8px; }
.sgm-header-progress { font-size: 13px; color: var(--ink-dim); }
/* Resume/Map (or /try's Switch topic) stay labeled pills — the thing
   someone actually came here to do. Share/Download live together in one
   small pill-shaped action group (.sgm-action-group) instead of as two
   separate free-floating circles — one cohesive "more actions" control,
   clearly secondary to navigation rather than two icons bolted on next
   to it. */
.sgm-header-actions { display: flex; gap: 8px; align-items: center; }
.sgm-header-actions .btn { font-size: 12px; padding: 6px 12px; }
.sgm-action-group {
  display: flex;
  align-items: center;
  gap: 1px;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px;
}
.sgm-action-group-divider { width: 1px; height: 14px; background: var(--border); margin: 0 1px; flex-shrink: 0; }
.sgm-icon-action-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: none;
  border: none;
  color: var(--ink-dim);
  cursor: pointer;
  flex-shrink: 0;
  padding: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
.sgm-icon-action-btn:hover { background: var(--card); color: var(--accent); }

/* Compact "at a glance" status pips — one per topic, same status data the
   map nodes themselves use. Hollow = not opened, filled = opened, filled
   teal = done: fill-vs-outline reads at a glance with no color legend
   needed. */
.sgm-progress-dots { display: flex; align-items: center; gap: 5px; }
.sgm-progress-dot { width: 7px; height: 7px; border-radius: 50%; background: none; border: 1.5px solid var(--border); flex-shrink: 0; box-sizing: border-box; }
.sgm-progress-dot-filled { background: var(--accent); border-color: var(--accent); }
.sgm-progress-dot-complete { background: var(--teal, #14b8a6); border-color: var(--teal, #14b8a6); }

/* No heavy card/border around the map anymore — a touch of whitespace is
   the only separation from the rest of the page, so this reads as the
   page's own environment rather than a component sitting on top of it. */
.sgm-map-shell {
  padding: clamp(6px, 1.5vw, 14px) 4px 4px;
  margin-bottom: 10px;
}
/* A container-query context — everything below sizes itself in cqw
   ("% of .sgm-map's own rendered width") instead of fixed px, so Cardie,
   his halo, his speech bubble, and every card's padding/icon/type all
   shrink in the exact same proportion the map itself does, at any
   width, rather than each being pinned to its own fixed number that
   only happened to look right at one particular size. clamp(mobile,
   Xcqw, desktop) is the pattern used throughout: Xcqw is the true
   continuous rate (this value's old fixed size, as a % of the map's
   600px desktop cap); the two bounds are the old fixed mobile and
   desktop values, kept as a legibility floor and a "never bigger than
   the original design" ceiling around that continuous middle. */
.sgm-map {
  position: relative;
  width: min(600px, 100%);
  aspect-ratio: 1 / 1;
  margin: 0 auto;
  container-type: inline-size;
  container-name: sgm-map;
}
/* A barely-there ambient wash behind the whole map — two soft, blurred
   glows (Cardie's own blue, teal) sitting further back than the "hub"
   glow under him, positioned toward corners the polygon math (locked,
   untouched) never puts a card in. This is what actually answers the
   "empty space" feeling: not shrinking or moving anything, just quietly
   filling the negative space around the fixed layout with depth instead
   of flat color. Kept topic-agnostic rather than literally water/weather
   -themed — a real generated guide can be about anything, and there's no
   reliable subject signal on the content to theme it by (see
   fallbackIcon's own reasoning in study-guide-view.mjs) — so every guide
   gets the same restrained atmosphere rather than a good treatment for
   one demo topic and a wrong one everywhere else.
   Solid blurred circles, not a radial-gradient fading to transparent —
   a low-contrast gradient stop set this subtle bands/rings visibly under
   `filter: blur()` in testing (worse in light mode, where it also read
   as a muddy gray cast over the cards), where a plain blurred solid
   shape renders as a clean, smooth falloff instead. z-index -1 keeps
   both behind the connectors/cards/Cardie regardless of their own
   stacking (1, 2, and SVG default respectively) without needing to
   touch any of that stacking itself. */
.sgm-map::before,
.sgm-map::after {
  content: "";
  position: absolute;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  width: 34%;
  height: 34%;
  filter: blur(38px);
}
.sgm-map::before {
  top: -4%;
  left: -4%;
  background: var(--accent);
  opacity: 0.14;
}
.sgm-map::after {
  bottom: -4%;
  right: -4%;
  background: var(--teal, #14b8a6);
  opacity: 0.12;
}
.sgm-connectors { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.sgm-connector-line {
  fill: none;
  stroke: var(--border);
  stroke-width: 0.4;
  stroke-dasharray: 0.5 3;
  stroke-linecap: round;
  opacity: 0.75;
  transition: stroke 0.15s ease, stroke-width 0.15s ease, opacity 0.15s ease;
}
/* A node's own path(s) light up on hover/focus — ties the dotted lines to
   an actual interaction instead of just sitting there decoratively. */
.sgm-connector-line.sgm-connector-active { stroke: var(--accent); stroke-width: 0.75; opacity: 1; }

.sgm-cardie-wrap {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1px, 0.333cqw, 2px);
  /* 10% narrower than the original 28cqw — this is what actually caps
     the bubble's own width (see .sgm-cardie-bubble's max-width:100%
     below), so shrinking it here is what shrinks the bubble box itself. */
  width: 25.2cqw;
  text-align: center;
  z-index: 2;
}
/* A quiet round platform under Cardie — reads as "a spot he's standing
   in," and now also as the map's own center of gravity: a soft radial
   glow (brighter at the middle, fading out) rather than a flat tint,
   plus a fainter, wider halo behind it for a touch more depth. Sized
   (and positioned) off cqw so it shrinks right along with Cardie's own
   .sgm-cardie-home below, instead of the two having separate numbers
   that can drift out of proportion at some width. */
.sgm-cardie-wrap::before {
  content: "";
  position: absolute;
  top: clamp(-28.6px, -4.766cqw, -20.9px);
  left: 50%;
  transform: translateX(-50%);
  width: clamp(136.4px, 30.8cqw, 184.8px);
  height: clamp(136.4px, 30.8cqw, 184.8px);
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent) 9%, transparent) 0%, transparent 68%);
  z-index: -1;
}
.sgm-cardie-wrap::after {
  content: "";
  position: absolute;
  top: clamp(-11px, -1.834cqw, -7.7px);
  left: 50%;
  transform: translateX(-50%);
  width: clamp(94.6px, 21.266cqw, 127.6px);
  height: clamp(94.6px, 21.266cqw, 127.6px);
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  z-index: -1;
}
/* Cardie himself and his halo above are both 10% bigger than the
   original design (each clamp bound scaled by 1.1×) — kept as a flat
   multiplier on every number rather than just the desktop max, so the
   two stay in the same proportion to each other at every width instead
   of the increase only showing up on large screens. */
.sgm-cardie-home {
  position: relative;
  width: clamp(74.8px, 16.866cqw, 101.2px);
  height: clamp(74.8px, 16.866cqw, 101.2px);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.sgm-cardie-home::after {
  content: "";
  position: absolute;
  bottom: clamp(7.7px, 1.834cqw, 11px);
  left: 50%;
  width: clamp(30.8px, 7.334cqw, 44px);
  height: clamp(5.5px, 1.284cqw, 7.7px);
  transform: translateX(-50%);
  background: color-mix(in srgb, var(--ink) 10%, transparent);
  border-radius: 50%;
  filter: blur(2.5px);
}
.sgm-cardie-img { width: clamp(48.4px, 11.734cqw, 70.4px); height: clamp(48.4px, 11.734cqw, 70.4px); object-fit: contain; position: relative; }
.sgm-cardie-img-wizard { width: clamp(57.2px, 14.3cqw, 85.8px); height: clamp(57.2px, 14.3cqw, 85.8px); }
.sgm-cardie-img-sm { width: 40px; height: 40px; flex-shrink: 0; }
/* Cardie's own map-screen line, styled as a small cloud he's actually
   speaking through — deliberately its own class (not the plain
   .sgm-bubble used on the concept screen's row) so this shape stays
   scoped to exactly this one spot. Kept small and quiet (light text, a
   soft near-white fill, a hairline shadow) so it reads as secondary to
   Cardie himself rather than competing with him for attention. Uneven
   corner radii — not the same value on every corner — are what keep
   this from reading as a plain pill/oval; a real cloud silhouette would
   need SVG/clip-path, but this is enough of a nudge off "rounded
   rectangle" to sell "soft" without adding any actual decoration. */
.sgm-cardie-bubble {
  position: relative;
  isolation: isolate;
  /* .sgm-cardie-wrap is now the one place this bubble's width is
     actually set (28cqw) — filling it here just means the bubble
     scales down in lockstep with it instead of the two having
     separate numbers that can drift apart. */
  max-width: 100%;
  margin: 0 auto;
  padding: clamp(5.4px, 1.05cqw, 6.3px) clamp(9.9px, 2.1cqw, 12.6px);
  background: color-mix(in srgb, var(--accent) 4%, var(--card-elevated));
  border: 1px solid color-mix(in srgb, var(--accent) 10%, var(--border));
  border-radius: 16px 20px 18px 14px / 12px 16px 14px 10px;
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.08));
  pointer-events: none;
}
/* Text and box both 10% smaller than the original design (every clamp
   bound above and below scaled by 0.9×, same flat-multiplier approach
   used for Cardie's own 10% increase) — kept proportional at every
   width rather than just shrinking the desktop size. */
.sgm-cardie-bubble-text {
  display: block;
  font-size: clamp(9.9px, 1.8cqw, 10.8px);
  font-weight: 600;
  line-height: 1.3;
  text-align: center;
  color: var(--ink-dim);
}
/* The pointer: a small square rotated to a diamond, tucked half behind
   the bubble's own top edge (z-index -1, contained by isolation:isolate
   above so it can't slip behind anything outside this one element) so
   only its lower corner shows — reading as a little tip aimed up at
   Cardie rather than a free-floating diamond. */
.sgm-cardie-bubble::after {
  content: "";
  position: absolute;
  top: clamp(-4.5px, -0.75cqw, -3.6px);
  left: 50%;
  width: clamp(7.2px, 1.65cqw, 9.9px);
  height: clamp(7.2px, 1.65cqw, 9.9px);
  background: inherit;
  border-left: inherit;
  border-top: inherit;
  border-radius: 3px 0 0 0;
  transform: translateX(-50%) rotate(45deg);
  z-index: -1;
}
.sgm-bubble {
  position: relative;
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 6px 12px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  pointer-events: none;
}
/* One consistent card shape carries every concept — visual interest comes
   from the icon badge, status, and connector line, not from varying the
   node's own silhouette (six different shapes read as messier, not more
   sophisticated, once real titles/hooks are in them). */
.sgm-node {
  position: absolute;
  transform: translate(-50%, -50%) scale(var(--sgm-scale, 1));
  display: flex;
  align-items: flex-start;
  gap: clamp(4px, 1.667cqw, 10px);
  /* A percentage of .sgm-map's own box, not a fixed pixel size — this is
     the exact fraction CARD_WIDTH_PCT in study-guide-view.mjs already
     assumes the card occupies when it computes the polygon's overlap-
     safety radius (176 / 600 = this same 29.3333%). A fixed px width
     here matched that assumption only while the map itself was still
     600px (i.e. only ever true on desktop); below that, the map shrinks
     (width: min(600px, 100%)) but a fixed-px card wouldn't, so the real
     on-screen fraction quietly grew past what the radius math had
     accounted for — cards ended up closer together, and near the edge,
     clipped by the viewport. Scaling the card itself keeps the fraction
     — and so the guaranteed no-overlap radius — true at every width;
     see the @media block below for the (purely cosmetic) type/icon
     sizing that keeps a now-smaller card readable. */
  width: 29.3333%;
  /* Height stays auto, sized to each card's own content — forcing every
     card to the tallest one's height (the "recommended" state's extra
     eyebrow/CTA lines) left the other five with a visibly empty gap
     below their shorter content. The connector's own edge-anchor math
     (see renderSpatialMap) measures each card's REAL rendered box, so a
     shorter card just gets a shorter offset from its vertex — the
     hexagon's vertex positions (equal radius, equal angles) don't depend
     on every card being the same height for the center-point geometry to
     be regular; only the polygon radius itself (see polygonRadiusPercent)
     assumes the tallest possible card, as a safety margin against
     whichever one is currently the taller "recommended" card. */
  padding: clamp(8px, 2cqw, 12px) clamp(6px, 2.5cqw, 15px);
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: clamp(10px, 2.667cqw, 16px);
  cursor: pointer;
  text-align: left;
  color: var(--ink);
  font-family: inherit;
  box-shadow: 0 2px 7px -3px rgba(0, 0, 0, 0.14);
  z-index: 1;
  overflow: hidden;
  transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease, background 0.15s ease;
}
.sgm-node:hover, .sgm-node:focus-visible {
  border-color: var(--accent);
  transform: translate(-50%, -50%) translateY(-3px) scale(calc(var(--sgm-scale, 1) * 1.03));
  box-shadow: 0 12px 22px -8px rgba(0, 0, 0, 0.3);
  outline: none;
  z-index: 3;
}
/* Hovering one destination gently recedes the others — a soft focus
   effect rather than a static grid of equally-loud cards. */
.sgm-map:has(.sgm-node:hover) .sgm-node:not(:hover) { opacity: 0.55; }
/* A hairline ring around every badge, same weight regardless of which of
   the 8 fallback icons (or a topic's own concept.icon) landed on this
   card — that ring, not the glyph inside it, is what makes the set read
   as one cohesive system rather than a grab-bag of shapes at a glance. */
.sgm-node-icon-badge {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(22px, 5cqw, 30px);
  height: clamp(22px, 5cqw, 30px);
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--card);
  border: 1px solid var(--border);
  color: var(--ink-dim);
}
/* A small corner badge marks "done" — the icon itself stays the topic's
   own identity rather than being swapped out for a checkmark. Teal, not
   Cardie's own accent blue, so it reads as the same "done" signal the
   progress dots already use (.sgm-progress-dot-complete) instead of
   blending into every other blue thing on the card (recommended eyebrow,
   CTA, hover border). */
.sgm-node-done-badge {
  position: absolute;
  bottom: clamp(-2px, -0.333cqw, -1px);
  right: clamp(-2px, -0.333cqw, -1px);
  width: clamp(11px, 2.5cqw, 15px);
  height: clamp(11px, 2.5cqw, 15px);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--teal, #14b8a6);
  color: #fff;
  border: 2px solid var(--card-elevated);
}
/* "In progress" (opened, not yet checked off) gets a plain dot in the
   same spot — no icon, smaller than the done checkmark — so it never
   reads as "finished" but still clearly isn't "untouched" either. Same
   accent blue the dot row uses for this exact state (.sgm-progress-dot-
   filled), kept deliberately quieter (smaller, no ring) than the
   recommended card's own use of that color. */
.sgm-node-progress-badge {
  position: absolute;
  bottom: 0;
  right: 0;
  width: clamp(7px, 1.5cqw, 9px);
  height: clamp(7px, 1.5cqw, 9px);
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--card-elevated);
}
.sgm-node-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: clamp(1px, 0.333cqw, 2px); padding-top: clamp(1px, 0.333cqw, 2px); }
.sgm-node-eyebrow { font-size: clamp(8px, 1.583cqw, 9.5px); font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--accent); margin-bottom: 1px; }
.sgm-node-title {
  font-size: clamp(11px, 2.25cqw, 13.5px);
  font-weight: 700;
  letter-spacing: -0.005em;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.sgm-node-hook {
  font-size: clamp(9px, 1.917cqw, 11.5px);
  color: var(--ink-dim);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Not started stays visually calm (default neutral border); in-progress
   gets a small corner dot (.sgm-node-progress-badge, added in
   renderSpatialMap) rather than a border change, so it reads as "you've
   been here" without competing with "recommended" for the loudest
   non-done state. "Done" borrows the dot row's own teal
   (.sgm-progress-dot-complete) rather than Cardie's blue — blue is
   already doing double duty for hover, recommended, and in-progress on
   this same map, so done needs a color of its own to actually register
   as a different state at a glance, not just a quieter version of the
   same one. */
.sgm-node-complete { border-color: color-mix(in srgb, var(--teal, #14b8a6) 45%, var(--border)); }
.sgm-node-in_progress { border-color: color-mix(in srgb, var(--accent) 22%, var(--border)); }
/* The one card that gets to look like the obvious next move: a warmer
   tinted surface (not just a swapped border color), a small permanent
   lift, and a left accent stripe — Cardie's own blue, not a generic
   focus-ring turquoise. */
.sgm-node-recommended {
  background: color-mix(in srgb, var(--accent) 9%, var(--card-elevated));
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  border-left: clamp(2px, 0.5cqw, 3px) solid var(--accent);
  padding-left: clamp(6px, 2cqw, 12px);
  box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--accent) 55%, transparent);
}
.sgm-node-recommended .sgm-node-title { font-weight: 800; }
.sgm-node-recommended .sgm-node-icon-badge { background: color-mix(in srgb, var(--accent) 18%, var(--card)); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); color: var(--accent); }
.sgm-node-cta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-top: 3px;
  font-size: clamp(9.5px, 1.833cqw, 11px);
  font-weight: 800;
  color: var(--accent);
}
.sgm-node-cta-arrow { transition: transform 0.15s ease; }
.sgm-node:hover .sgm-node-cta-arrow, .sgm-node:focus-visible .sgm-node-cta-arrow { transform: translateX(2px); }

/* The full hook, dropped down under a card on hover/focus — a sibling of
   the node (see study-guide-view.mjs) so it isn't clipped by the node's
   own overflow:hidden safety net. */
.sgm-node-dropdown {
  position: absolute;
  width: clamp(150px, 29.667cqw, 178px);
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 10px 12px;
  font-size: clamp(11.5px, 2cqw, 12px);
  line-height: 1.4;
  color: var(--ink);
  text-align: left;
  box-shadow: 0 10px 20px -8px rgba(0, 0, 0, 0.3);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 4;
}
/* Below a top-half node, above a bottom-half one — either way it opens
   away from Cardie's own bubble at the center instead of over it. */
.sgm-node-dropdown-below { transform: translate(-50%, clamp(46px, 9cqw, 54px)); }
.sgm-node-dropdown-above { transform: translate(-50%, calc(-100% - clamp(46px, 9cqw, 54px))); }
.sgm-node:hover + .sgm-node-dropdown,
.sgm-node:focus-visible + .sgm-node-dropdown {
  opacity: 1;
  visibility: visible;
}
/* A touch tap fires hover and click together, so the dropdown never gets
   a chance to actually be read before navigation happens anyway — on a
   device with no real hover, skip it rather than have it flash and do
   nothing. The full text is one tap away either way, inside the topic. */
@media (hover: none) {
  .sgm-node-dropdown { display: none; }
}

/* A small supporting control, not a second primary action — text-only,
   same weight as Cardie's own choice links, so it reads as "one more
   option" rather than competing with the map itself for attention. */
.sgm-surprise-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 8px auto 0;
  background: none;
  border: none;
  padding: 4px 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-dim);
  cursor: pointer;
  font-family: inherit;
}
.sgm-surprise-btn:hover { color: var(--accent); }

/* Same radial map as desktop, just smaller — .sgm-node's own width is
   already a percentage of .sgm-map (see the comment above the base
   .sgm-node rule), so it shrinks continuously right along with the map
   itself and the polygon's overlap-safety math stays true at any size.
   Nothing here touches position, width, or the connector/glow layers;
   only cosmetic detail sizing (icon, type, padding, Cardie himself)
   that reads better once a card is this small on-screen. */
/* Everything above (padding, icon size, type, Cardie, his halo, his
   bubble) now scales continuously in cqw, so there's no longer a
   breakpoint where most of the figure suddenly jumps to a different
   size — the one thing left that genuinely needs a hard switch, not
   just a smaller number, is this: side-by-side (icon column + text
   column) stops working once the card itself is narrow enough that the
   icon and its gap eat most of it, leaving too little width for even a
   short title to wrap sensibly. A @container query (keyed to the map's
   own rendered width, not the viewport) swaps to icon-above-a-centered-
   column at that point — still scaling continuously via the same clamp()
   rules either side of the switch. */
@container sgm-map (max-width: 560px) {
  .sgm-node { flex-direction: column; align-items: center; text-align: center; }
  /* No extra padding-left override needed here — .sgm-node-recommended's
     own clamp() padding-left (set alongside the desktop border-left
     above) was only ever there to make room for that stripe; once the
     stripe moves to the top edge instead, the card's normal symmetric
     padding (inherited from .sgm-node) is already correct on its own. */
  .sgm-node-recommended { border-left: none; border-top: clamp(2px, 0.5cqw, 3px) solid var(--accent); padding-left: clamp(6px, 2.5cqw, 15px); }
  .sgm-node-hook { display: none; }
  .sgm-node-cta { justify-content: center; }
}

/* Below MOBILE_STACK_BREAKPOINT (study-guide-view.mjs), renderSpatialMap
   builds this plain stacked list instead — see renderStackedMap there
   for why. Cards render at full width here (not the diamond's narrow
   29.3333%-of-map width), so the side-by-side icon+text layout always
   has room; there's no need for the @container swap above, and these
   classes only ever exist in the DOM when JS has already chosen this
   layout, so nothing here needs its own width gate. A container context
   of its own (container-type below) keeps every clamp(...cqw...) rule
   .sgm-node-stack inherits from the base .sgm-node well-defined instead
   of resolving against no container at all; in practice the numbers
   this narrow always land on each rule's own mobile floor, which is
   exactly the small, legible sizing stacked mode wants anyway. */
.sgm-map-shell-stacked { padding: 4px; container-type: inline-size; }
.sgm-stack-cardie {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  text-align: center;
}
.sgm-stack-cardie .sgm-cardie-home { width: 64px; height: 64px; }
.sgm-stack-cardie .sgm-cardie-img { width: 42px; height: 42px; }
.sgm-stack-cardie .sgm-cardie-img-wizard { width: 50px; height: 50px; }
/* A plain label here, not the full cloud-bubble treatment (used at the
   radial map's center, where it needs a pointer to visually connect it
   to Cardie sitting right above it) — Cardie has his own row above the
   list here, with nothing for a pointer to aim at. */
.sgm-stack-cardie .sgm-cardie-bubble {
  max-width: none;
  filter: none;
  border: none;
  background: none;
  padding: 0;
}
.sgm-stack-cardie .sgm-cardie-bubble::after { display: none; }
.sgm-stack-cardie .sgm-cardie-bubble-text { font-size: 13px; color: var(--ink-dim); }
.sgm-stack-list { display: flex; flex-direction: column; align-items: center; }
.sgm-node-stack {
  position: static;
  width: 100%;
  transform: none;
}
.sgm-node-stack:hover, .sgm-node-stack:focus-visible { transform: translateY(-2px); }
/* A short dotted line between each pair of cards, in the same dotted
   language as the radial map's own connector lines (.sgm-connector-line)
   — so the stacked list still reads as one connected path through the
   topic rather than a flat, disconnected list of boxes. */
.sgm-stack-connector {
  width: 0;
  height: 16px;
  border-left: 2px dotted var(--border);
  margin: 2px 0;
}

@media (max-width: 620px) {
  .sgm-map-shell { padding: 8px 10px 4px; }
  .sgm-bubble { font-size: 11px; padding: 5px 9px; }
  .sgm-lens-row { gap: 6px; }
  .sgm-lens-btn { flex-direction: column; align-items: flex-start; gap: 4px; padding: 8px; }
  .sgm-lens-btn-label { font-size: 12px; }
  .sgm-lens-btn-detail { font-size: 9.5px; }
}

/* ---- Concept screen: predict beat + interaction lenses ---- */
.sgm-map-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  color: var(--ink-dim);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  padding: 0;
  margin-bottom: 14px;
}
.sgm-map-link:hover { color: var(--ink); }
.sgm-concept-cardie { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
.sgm-concept-cardie .sgm-bubble { flex: 1; }

.sgm-predict {
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 18px;
  margin-bottom: 20px;
}
.sgm-predict-q { font-size: 15px; font-weight: 700; margin: 0 0 12px; }
.sgm-predict-options { display: flex; flex-direction: column; gap: 8px; }
.sgm-predict-option {
  text-align: left;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
}
.sgm-predict-option:hover:not(:disabled) { border-color: var(--accent); }
.sgm-predict-option:disabled { cursor: default; }
.sgm-predict-option.sgm-correct { border-color: var(--teal, #14b8a6); background: rgba(20, 184, 166, 0.12); }
/* The option actually clicked, when it wasn't the right one — dimmed and
   struck through rather than flagged red (this is a guess before reading,
   not a graded test), so it still reads clearly as "this is the one you
   picked" against the correct answer highlighted teal beside it. */
.sgm-predict-option.sgm-picked { border-color: var(--border); background: var(--card-elevated); opacity: 0.55; text-decoration: line-through; }
.sgm-predict textarea {
  width: 100%;
  resize: vertical;
  min-height: 60px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--ink);
  font-family: inherit;
  font-size: 14px;
  padding: 10px;
  margin-bottom: 10px;
}
.sgm-predict-reveal { display: none; flex-direction: column; align-items: flex-end; gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); font-size: 14px; }
.sgm-predict-reveal.visible { display: flex; }
.sgm-predict-reveal-text { margin: 0; width: 100%; text-align: left; }
.sgm-predict-continue { margin: 0; }

/* Three small cards, not settings-menu pills — a title plus a one-line
   reason to pick it, so "how do you want this one?" reads as a real
   choice rather than a row of filter chips. */
.sgm-lens-row { display: flex; gap: 8px; margin-bottom: 16px; }
.sgm-lens-btn {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--ink-dim);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.sgm-lens-btn:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
.sgm-lens-btn.active {
  background: color-mix(in srgb, var(--accent) 10%, var(--card));
  border-color: var(--accent);
  color: var(--ink);
}
.sgm-lens-btn.active .sgm-lens-btn-label { color: var(--accent); }
.sgm-lens-btn-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sgm-lens-btn-label { font-size: 13px; font-weight: 700; color: var(--ink); }
.sgm-lens-btn-detail { font-size: 10.5px; color: var(--ink-dim); }
.sgm-lens-content {
  background: var(--card-elevated);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 18px;
  font-size: 15px;
  line-height: 1.55;
}
.sgm-lens-content p { margin: 0 0 12px; }
.sgm-lens-content p:last-child { margin-bottom: 0; }
.sgm-lens-why { color: var(--ink-dim); }
.sgm-lens-steps { margin: 0; padding-left: 20px; }

.sgm-mix-up { background: var(--honey-pale); color: var(--honey-ink); border-radius: 10px; padding: 10px 14px; font-size: 14px; margin-top: 12px; }
.sgm-mix-up strong { display: block; margin-bottom: 2px; }

.sgm-qa-question { font-size: 15px; font-weight: 700; margin: 0 0 12px; }
.sgm-qa-textarea {
  display: block;
  width: 100%;
  resize: vertical;
  min-height: 84px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--ink);
  font-family: inherit;
  font-size: 14px;
  line-height: 1.5;
  padding: 12px 14px;
  margin-bottom: 16px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.sgm-qa-textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.sgm-qa-textarea::placeholder { color: var(--ink-dim); }
.sgm-qa-reveal-btn { display: block; }
.sgm-qa-answer { display: none; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.sgm-qa-answer.visible { display: block; }
.sgm-qa-answer-text { font-size: 14px; margin: 0; }

