/* @font-face declarations — actual .woff2 files live in ../fonts/
   (this file sits in assets/css/, the fonts sit in assets/fonts/, hence
   the ../ — extracted from what used to be inline base64 here).
   font-display: swap avoids hiding text for the full load window
   (previously unset -> default 'auto'). */
@font-face {
  font-family: 'Tinos'; font-style: normal; font-weight: 400;
  src: url('../fonts/tinos-regular.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Tinos'; font-style: normal; font-weight: 700;
  src: url('../fonts/tinos-bold.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Tinos'; font-style: italic; font-weight: 400;
  src: url('../fonts/tinos-italic.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Tinos'; font-style: italic; font-weight: 700;
  src: url('../fonts/tinos-bold-italic.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Pixelify Sans'; font-style: normal; font-weight: 400;
  src: url('../fonts/pixelify-sans-regular.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Pixelify Sans'; font-style: normal; font-weight: 600;
  src: url('../fonts/pixelify-sans-semibold.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'ITC Souvenir Demi'; font-style: normal; font-weight: 600;
  src: url('../fonts/itc-souvenir-demi.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'ITC Souvenir Medium'; font-style: normal; font-weight: 500;
  src: url('../fonts/itc-souvenir-medium.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'ITC Avant Garde Gothic Demi'; font-style: normal; font-weight: 600;
  src: url('../fonts/itc-avant-garde-gothic-demi.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'ITC Avant Garde Gothic Book'; font-style: normal; font-weight: 400;
  src: url('../fonts/itc-avant-garde-gothic-book.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Press Start 2P'; font-style: normal; font-weight: 400;
  src: url('../fonts/press-start-2p-regular.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Courier Prime'; font-style: normal; font-weight: 400;
  src: url('../fonts/courier-prime-regular.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Courier Prime'; font-style: normal; font-weight: 700;
  src: url('../fonts/courier-prime-bold.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'Courier Prime'; font-style: italic; font-weight: 400;
  src: url('../fonts/courier-prime-italic.woff2') format('woff2');
  font-display: swap;
}
@font-face {
  font-family: 'DXS Yagi Double'; font-style: normal; font-weight: 400;
  src: url('../fonts/dxs-yagi-double.woff2') format('woff2');
  font-display: swap;
}

    :root {
      --bg-cream: #F4F1EA;
      --bg-card: #EFECE6;
      --paper-white: #FAF9F5;
      --text-dark: #222222;
      --text-muted: #76736E;
      --border-dark: #222222;
      --accent-orange: #D9531E;
      /* Hard-shadow and structural-divider tones split out from
         --border-dark rather than reusing it, specifically for dark
         mode's sake — see html.dark-mode below for why. Light-mode
         values here just match what --border-dark and
         .quick-actions-row's old hardcoded divider color already were,
         so light mode's actual rendered output doesn't change at all. */
      --shadow-hard: #222222;
      --divider-muted: rgba(34, 34, 34, 0.18);
      /* Works Cited drawer's manila-folder palette — a fixed '70s-office
         theme of its own (per spec), not derived from the rest of the
         Document Center's palette, but still swapped for a darker
         analogous pair under html.dark-mode below so the drawer doesn't
         glare against a dark editor. */
      --manila-primary: #E8DFD0;
      --manila-accent: #C8BCA8;
      /* '70s Document Center palette — these five names are shared CSS
         custom properties consumed by the exact same bubble/panel button
         classes in BOTH the main editor and the Build-an-Essay workshop
         (the workshop deliberately reuses the editor's markup/classes for
         identical animation/sound). Defining the '70s values here at
         :root makes them the Document Center's colors by default; the
         `.workshop-wrapper` block further down re-declares all five
         names with the arcade palette so the workshop subtree overrides
         them locally instead of inheriting these. Never rename the
         variables themselves — only the two scopes' values may differ. */
      --thesaurus-blue: #2A5B75;      /* Dusty Denim */
      --thesaurus-blue-dark: #1E4154;
      --spell-teal: #5B7036;          /* Avocado / Olive */
      --spell-teal-dark: #415026;
      --write-burgundy: #8A2B2B;      /* Corduroy Red */
      --write-burgundy-dark: #631E1E;
      --beta-stamp-color: var(--write-burgundy-dark); /* dark-mode override below — see html.dark-mode */
      --rhyme-purple: #523B5A;        /* Deep Vintage Plum */
      --rhyme-purple-dark: #3B2A40;
      --speak-mustard: #D49B22;       /* Rich Harvest Gold */
      --speak-mustard-dark: #986F18;
      --shadow-soft: rgba(0, 0, 0, 0.05);
      /* BUILD-AN-ESSAY WORKSHOP — its own accent, scoped to the workshop
         view rather than a global reskin. A saturated retro cobalt (video-
         game "power" blue) paired with a brighter CRT-glow cyan for
         highlights/active states, keeping the rest of the app's existing
         orange-accented palette untouched. */
      --workshop-blue: #1B40D8;
      --workshop-blue-dark: #15369A;
      --workshop-cyan: #4FE0E8;
    }

    /* 8-BIT ARCADE ACTION-BUBBLE PALETTE — re-declares the same five
       shared custom properties above, scoped via `body.workshop-mode`
       (toggled in JS whenever #workshop-view's hidden state changes —
       see the MutationObserver near positionBrandHeaderLine's own) so
       this whole tree, including the side panels (Thesaurus/Did You
       Mean/Grammar/Rhymes) which are shared singleton elements living
       OUTSIDE both #editor-view and #workshop-view in the DOM, renders in
       arcade CRT colors while Build-an-Essay is the active view. The
       Document Center (which reads the plain :root values whenever this
       body class is absent) is completely unaffected. The "-dark" name is
       reused here for each color's accent/highlight (hover/active) state
       — brighter than the base, not darker, matching the arcade's
       neon-on-hover language rather than the Document Center's
       dim-on-hover one. */
    body.workshop-mode {
      --thesaurus-blue: #1B40D8;   --thesaurus-blue-dark: #5A82FF;
      --spell-teal: #008844;       --spell-teal-dark: #00E676;
      --write-burgundy: #C41E3A;   --write-burgundy-dark: #FF5252;
      --rhyme-purple: #7B1FA2;     --rhyme-purple-dark: #E040FB;
      --speak-mustard: #D87D00;    --speak-mustard-dark: #FFC107;
    }

    /* '70s hi-fi dark palette (Phase 1) — replaces the previous cool
       slate/lavender dark mode with warm espresso/walnut/parchment
       tones, matching the daytime palette's own warmth instead of
       reading as a generic "modern dark UI." One deliberate deviation
       from the warm-everything brief: --border-dark stays tied to
       --text-dark's new value (bright parchment) rather than dropping
       to a muted bronze — border-dark moonlights as the fill color for
       15+ inverted/active button states across the app (hover states,
       .active tabs, etc.), not just an outline color, so a low-contrast
       bronze there would make all of those nearly invisible against a
       dark background. The muted-bronze tone the brief wanted for
       borders instead went to the new --divider-muted (genuinely
       decorative structural lines only, never a fill) and --shadow-hard
       (drop shadows need to be DARKER than the card to read as depth,
       the opposite lightness direction from a border that needs to be
       LIGHTER than the background to be visible — the same value can't
       serve both well, which is why they're split into their own
       variables here rather than reusing --border-dark for both like
       light mode gets away with). */
    html.dark-mode {
      --bg-cream: #1B1816;      /* deep warm espresso, was a cool slate #1c1a22 */
      --bg-card: #262220;       /* dark walnut, was cool-toned #262330 */
      --paper-white: #231F1D;   /* distinct from --bg-card now (both were #262330 before) — gives inputs/pills their own slightly darker surface instead of blending into cards */
      --text-dark: #EFE8DB;     /* warm parchment cream, was cool lavender-white #EDE9F2 */
      --text-muted: #B0A597;    /* warm taupe, was cool grey-taupe #9a94a8 */
      --border-dark: #EFE8DB;   /* kept equal to the new --text-dark, same relationship light mode already has (both #222222) — see comment above */
      --accent-orange: #E05A36; /* terracotta, dark-mode-specific — light mode's #D9531E was previously inherited unchanged here */
      --shadow-hard: #0D0C0B;   /* near-black espresso, deliberately darker than --bg-cream so hard shadows still read as depth against a dark surface */
      --divider-muted: #3D3632; /* warm bronze-slate — solid, not translucent like light mode's rgba: a translucent near-black is invisible against a near-black background */
      --shadow-soft: rgba(0, 0, 0, 0.25);
      --workshop-blue: #4169E1;
      --workshop-blue-dark: #2947A3;
      --workshop-cyan: #6BEAF0;
      --manila-primary: #28231D; /* Deep Muted Amber/Charcoal — darkened, warm-toned folder resting on dark paper, was a lighter walnut #3A332C */
      --manila-accent: #4A3E31;  /* Warm Medium Bronze, was #4F4438 */
      --beta-stamp-color: #A83434; /* same hue/character as .btn-write-bubble's resting (non-hover) --write-burgundy (#8A2B2B), just lifted a bit lighter for this specific spot — a hardcoded hex rather than var(--write-burgundy) directly, since that variable isn't redefined in dark mode and reusing it verbatim would've kept the badge exactly as dim as light mode's own resting bubble color. */
    }

    /* Build-An-Essay's own dark-mode palette (Phase 2) — a 16-bit
       arcade console look, deliberately distinct from the Document
       Center's warm 70s dark mode above: saturated electric blue over
       deep indigo/navy instead of warm espresso/parchment. Gated on
       BOTH classes (html.dark-mode AND body.workshop-mode) so it only
       kicks in when dark mode and the workshop are simultaneously
       active; higher selector specificity than html.dark-mode alone
       (two classes vs one) means it correctly wins over those base
       values without needing !important. */
    html.dark-mode body.workshop-mode {
      --bg-cream: #12131C;      /* overall workshop view background */
      --bg-card: #222436;       /* nested/inner tier — hint panel fill, locked-node fill */
      --paper-white: #1A1B28;   /* main panel tier — stage card, panels */
      --text-dark: #E2E8F0;     /* phosphor crisp white */
      --text-muted: #8E9BB0;    /* muted steel blue */
      /* Kept bright/tied to --text-dark rather than the ticket's own
         muted blue-slate — border-dark doubles as the FILL color for
         several inverted/active states reachable from inside the
         workshop (Grammar tab, Comfort Zoom, Set Goal's tabs, bulk-
         action hover — see the identical reasoning on html.dark-mode
         above), which pair it with var(--paper-white) text and would
         go dark-on-dark if border-dark dropped to a low-contrast slate
         too. The actual "glowing blue-slate outline" the brief wants
         for real borders lives on --divider-muted instead, same split
         as the Document Center's own dark mode. */
      --border-dark: #E2E8F0;
      --divider-muted: #2D3248;
      --shadow-hard: #08080E;
      --workshop-blue: #2F63FF;
      --workshop-blue-dark: #2247B0;
      /* --workshop-cyan intentionally untouched — it's a glow/accent
         color (castle glow, done-checkmark tint, the DONE! ding flash),
         not mentioned in the brief, and its existing bright cyan
         already reads fine against the new deeper indigo background. */
    }

    body { transition: background-color 0.4s ease, color 0.4s ease; }

    * { box-sizing: border-box; margin: 0; padding: 0; }
    button, input, select, textarea { font: inherit; color: inherit; }

    body {
      background-color: var(--bg-cream);
      color: var(--text-dark);
      /* ITC Avant Garde Gothic Book as the app-wide UI-chrome default —
         covers "helper text" broadly (and doubles as Build-an-Essay's own
         instructional/callout/description body copy, which explicitly
         calls for the same face) without having to hunt down every small
         label individually. Elements with their own explicit font-family
         (document prose, MLA/Tinos text, panel headers, pixel-font
         flourishes, arcade Press Start 2P elements) are unaffected since
         those declarations take precedence over this inherited default. */
      font-family: 'ITC Avant Garde Gothic Book', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      line-height: 1.5; letter-spacing: 0.01em;
      min-height: 100vh;
      overflow-x: hidden;
    }

    /* GLOBAL FONT METRIC RESET — ITC Avant Garde Gothic has a noticeably
       higher internal x-height/bounding-box ratio than the system-sans
       fallback stack it replaced, which is what kept producing the
       "text sits off-baseline against its icon" bug this session hit
       repeatedly (docnav dots, save-card snippets, preferences toggles)
       — each fixed one component at a time by tightening that
       component's own line-height. This is the general-purpose version
       of that same fix, applied once at the element level instead of
       chasing each new occurrence individually. */
    p, span, h1, h2, h3, h4, h5, h6, label { line-height: 1.2; }
    /* Real document prose is the one deliberate exception — it's set in
       Georgia (never Avant Garde) specifically so it keeps its own
       carefully-tuned reading line-height (1.5 normal, 2 double-spaced
       MLA) rather than inheriting this UI-chrome reset. A directly
       targeted element selector like p/span/h1-h3 above overrides
       inherited line-height regardless of specificity, so these need
       their own explicit restatement rather than just relying on
       .editor-canvas's own line-height cascading down. */
    .editor-canvas p, .editor-canvas span,
    .editor-canvas h1, .editor-canvas h2, .editor-canvas h3 {
      line-height: inherit;
    }

    /* SCOPED INPUT BASELINE COMPENSATION — Avant Garde's tall internal
       metrics also throw off form fields specifically: the rendered
       glyph baseline sits visibly off from the blinking caret's vertical
       center. `line-height: normal` drops back to the font's own natural
       metric (rather than a fixed multiplier that doesn't account for
       it), and a small top nudge re-centers the optical result. Any
       field that already sets its own line-height/padding at a more
       specific selector (.doc-title-input, .workshop-textarea, the
       various panel/citation inputs, etc.) is unaffected — a class
       selector's longhand always wins over this element-level one for
       the same property, regardless of declaration order. */
    input, textarea {
      line-height: normal;
      padding-top: 0.15em;
    }

    /* FLEX CONTROL CENTERING — a baseline default for every interactive
       control so label text sits optically centered against its own
       box rather than riding high/low per Avant Garde's metrics.
       .badge/.pill-control don't exist as literal classes in this
       codebase (everything here is hand-built rather than pulled from a
       shared component library) — .howto-badge is this app's actual
       "badge" element, included alongside them so the rule has a real
       target. Buttons that deliberately lay out an icon + label pair
       with room to spare (not just a single centered label) set their
       own justify-content afterward, since a class selector's longhand
       always wins over this element-level one for the same property. */
    button, .badge, .pill-control, .howto-badge {
      display: inline-flex; align-items: center; justify-content: center;
      padding-top: 1px;
    }
    .workshop-add-item-btn { justify-content: flex-start; }

    .hidden { display: none !important; }

    /* HEADER */
    /* The plummet/rise transitions apply their transform to this wrapper,
       not #menu-view itself — #menu-view also contains .docnav-tree, which
       is position:fixed and needs to stay pinned to the viewport all the
       way through the transition. A transform on any ancestor (even one
       that isn't the direct parent) makes fixed descendants position
       relative to THAT ancestor instead of the viewport, which is exactly
       what was making the nav tree visibly jump. Keeping the transform
       scoped to a sibling wrapper means .docnav-tree never has a
       transformed ancestor at all. Deliberately left at browser-default
       block display (matching header/.menu-container's own default) so
       wrapping them changes nothing about the existing layout. */
    header {
      /* Pinned to the viewport top so it travels with the page the same
         way .docnav-tree already does, rather than scrolling away with a
         long document grid. Taken out of normal flow entirely, so
         .menu-content-wrapper needs matching top padding below to keep
         its content from sliding up underneath it — see there. */
      position: fixed; top: 0; left: 0; right: 0; z-index: 65;
      display: flex; justify-content: flex-end; align-items: center;
      /* Bottom padding trimmed to 5px so the header's own opaque
         background ends flush with where the header-line's flat
         segment actually renders (y=81, see .header-line-left/-right
         below) instead of extending 15px past it — that excess used
         to sit as a visible strip of background color between the
         line and the document panels below. The pencil and line are
         positioned via fixed top: values (not bottom:, see below) so
         shrinking this padding doesn't drag them along with it. */
      padding: 20px 24px 5px; height: 56px; box-sizing: content-box; background-color: var(--bg-cream);
    }
    /* Total rendered header height (56 content + 20 top + 5 bottom
       padding, content-box) — the exact amount .menu-content-wrapper
       needs pushed down now that the header no longer occupies its
       own space in flow. */
    #menu-view .menu-content-wrapper { padding-top: 81px; }
    /* Title, pencil, and both line segments all sit on one shared
       baseline (fixed top: values, see .brand-pencil and
       .header-line-left/-right below) so they read as genuinely
       resting on the same line rather than each being independently
       roughly-aligned. Both line segments
       and the pencil's horizontal position are computed in JS (see
       positionBrandHeaderLine) once the title's real rendered width is
       known — necessary because the pencil is rotated, and rotating an
       element changes its visual footprint but not its layout footprint,
       so a pure-CSS fit-content approach can never actually know where
       the visually-rotated tip ends up. */
    .brand-title {
      position: absolute; left: 24px; top: 28px;
      font-family: 'ITC Souvenir Demi', 'Georgia', serif;
      font-size: 2.1rem; font-weight: bold; line-height: 1;
      color: var(--text-dark);
    }
    /* Positioned against .brand-title itself (already position:absolute,
       so it's a valid containing block for this) rather than computed in
       JS like the header line/pencil above. left:100% (not a right:
       offset back over the title's own box) is what actually moves it
       off the title text entirely — it starts exactly at the title's own
       right edge (where "k" ends) and reads into the gap toward the
       pencil, rather than hanging back over the last letter. top stays
       matched to the title's own top so it's still "the same vertical
       position", just no longer stacked on the glyph itself.
       rotate(-3deg) tilts the left side down/right side up per spec;
       full opacity (not faded) — an earlier pass tried a translucent
       ink-stamp look here, but that's not what's wanted, so both color
       and border sit at opacity:1 on the container. Color uses its own
       --beta-stamp-color variable rather than reusing --write-burgundy
       or --write-burgundy-dark directly, because BOTH of those are
       shared by several hover/active states elsewhere and NEITHER is
       redefined in html.dark-mode — reusing either verbatim would've
       silently tied the badge's dark-mode look to whatever that shared
       variable happens to resolve to instead of a deliberate choice.
       Light mode: var(--write-burgundy-dark), the same deep maroon
       .btn-write-bubble turns on hover. Dark mode (see html.dark-mode):
       a dedicated brightened hex based on that same bubble's un-hovered/
       resting --write-burgundy hue, not its hover one — without having
       to touch either variable's other consumers app-wide to get there. */
    /* Fixed width/height (not auto/shrink-to-fit, as this used to be) —
       measured the old auto-sized box before this change and its content
       box was already within half a pixel of the text's own rendered
       width, i.e. essentially zero clearance from the frame by
       construction (any size the text happens to render at always
       exactly fills auto-sizing's content box). Pinning the box's own
       size and centering the text inside it with flex is what actually
       decouples the two, so the font can grow without the frame
       following it or the text crowding the border. */
    .brand-beta-badge {
      position: absolute; top: -8px; left: calc(100% + 6px); z-index: 5;
      width: 47px; height: 25px;
      display: flex; align-items: center; justify-content: center;
      transform: rotate(-3deg);
      opacity: 1;
      font-family: 'Courier Prime', 'Courier New', monospace;
      font-weight: 700; font-size: 0.85rem; letter-spacing: 0.08em;
      color: var(--beta-stamp-color);
      border: 1.5px solid var(--beta-stamp-color); border-radius: 4px;
      line-height: 1.3;
      white-space: nowrap; pointer-events: none;
    }
    /* Nudges just the glyphs 2px down within the badge's own flex-
       centered box, without touching the box's rotation transform above
       (a translateY on .brand-beta-badge itself would fight that). */
    .brand-beta-badge-text { position: relative; top: 1px; }
    .brand-pencil {
      /* top: fixed at 58px rather than tracking the header's own
         bottom edge (as bottom: used to) — decoupled on purpose so
         trimming the header's bottom padding above doesn't drag the
         pencil up with it. 58px is the pencil's exact prior absolute
         position (old 96px header height minus its old 14px bottom
         offset minus its own 24px height), so this preserves its
         visual placement pixel-for-pixel. */
      position: absolute; top: 58px; color: var(--text-dark);
      /* transform-origin bottom center means the rotation in the
         keyframes below pivots around this element's own bottom-center
         point — placed to coincide with the pencil's visual tip, that
         point stays visually stationary through the whole wobble, which
         is exactly what lets a single fixed x-position work for both
         the pencil and the line's endpoint despite the animation. */
      transform: translateX(-50%);
      transform-origin: bottom center;
      animation: brand-pencil-write 1.7s ease-in-out infinite;
    }
    .brand-pencil svg { display: block; width: 13px; height: 24px; }
    /* Tilts right now (positive rotation), reversed from the initial
       left-leaning version. */
    @keyframes brand-pencil-write {
      0%, 100% { transform: translateX(-50%) rotate(32deg); }
      25% { transform: translateX(-50%) rotate(27deg); }
      50% { transform: translateX(-50%) rotate(37deg); }
      75% { transform: translateX(-50%) rotate(29deg); }
    }
    /* '70s sweeping curve — each segment is flat near the pencil/title
       (preserving the same baseline the old straight 2px line sat on)
       and sweeps downward as it nears the outer edge of the viewport,
       reading as flowing off-screen rather than terminating in a hard
       stop. The container's top edge is deliberately kept at the exact
       same Y as the old line's top (bottom shifted down + height grown
       by the same amount) so the flat portion lines up pixel-for-pixel
       with where the straight line used to be; the extra height below
       is just room for the dip. preserveAspectRatio="none" lets the
       existing JS width-in-px logic (unchanged — still just sets
       .style.width to stop the curve exactly at the pencil's tip) scale
       the path horizontally without needing new positioning math. */
    .header-line-left, .header-line-right {
      /* top: fixed at 80px rather than tracking the header's own
         bottom edge (as bottom: used to) — same decoupling reason as
         .brand-pencil above. 80px is the line's exact prior absolute
         position (old 96px header height minus its own 16px height),
         preserving its visual placement pixel-for-pixel while the
         header's own background now ends 1px below it at y=81. */
      position: absolute; top: 80px; height: 16px;
      display: block; overflow: visible;
    }
    .header-line-left { left: 0; } /* width set by JS — stops exactly at the pencil's tip */
    .header-line-right { right: 0; } /* left set by JS — resumes a short gap after the pencil */
    .header-line-left path, .header-line-right path {
      /* Reverted to --border-dark (was --divider-muted) — this line
         reads as part of the brand-title/pencil's own bright ink, not
         a subdued structural divider like .quick-actions-row's, so it
         needs to match THEIR color (and --divider-muted's light-mode
         value is translucent, unlike border-dark's solid one — that
         translucency was the actual visible regression in light mode). */
      stroke: var(--border-dark); stroke-width: 2px; fill: none; stroke-linecap: round;
    }

    .btn-howto-icon {
      background: var(--paper-white); border: 2px solid var(--border-dark);
      cursor: pointer; font-size: 1rem; line-height: 1; color: var(--text-dark);
      width: 32px; height: 32px; border-radius: 6px;
      display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--shadow-hard);
      transition: transform 0.15s, background-color 0.15s, border-color 0.4s ease, box-shadow 0.4s ease;
    }
    .btn-howto-icon svg { fill: currentColor; }
    .btn-howto-icon:hover { background-color: var(--bg-card); transform: translateY(-1px); }

    /* MENU GRID */
    .menu-container { max-width: 1000px; margin: 0 auto; padding: 30px 20px; }
    .doc-search-row { margin-bottom: 20px; display: flex; align-items: center; gap: 14px; }
    .doc-search-input {
      width: 100%; max-width: 320px; flex-shrink: 0; padding: 9px 12px 7px; border: 2px solid var(--border-dark);
      border-radius: 20px; font-family: 'ITC Avant Garde Gothic Book', sans-serif;
      font-size: 0.8rem; background: var(--paper-white); color: var(--text-dark); outline: none;
      transition: background-color 0.4s ease, border-color 0.4s ease, color 0.4s ease;
    }
    .doc-search-input:focus { border-color: var(--accent-orange); }
    .doc-search-input::placeholder { color: #BBB8B2; }
    .grid-trash-btn {
      background: transparent; border: none; cursor: pointer;
      font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1px; font-weight: 700;
      color: var(--text-muted); display: flex; align-items: center; gap: 5px;
      padding: 4px 6px; border-radius: 4px; transition: background-color 0.4s ease, color 0.4s ease;
    }
    .grid-trash-btn:hover { background-color: var(--bg-card); color: var(--text-dark); }
    .grid-header-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; flex-shrink: 0; }
    .btn-empty-trash {
      background: var(--paper-white); border: 2px solid var(--accent-orange); color: var(--accent-orange);
      cursor: pointer; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 1px; font-weight: 700;
      /* Asymmetric padding (was a flat 5px), not transform — transform
         shifted the whole pill including its border down with the
         text, leaving the border sitting visibly lower than "← Back"
         even once the text itself lined up. Splitting the same 10px
         total top+bottom padding as 6.5/3.5 nudges just the text down
         1.5px while the border box stays exactly where it was. */
      padding: 6.5px 10px 3.5px; border-radius: 4px; transition: background-color 0.4s ease, color 0.4s ease;
    }
    .btn-empty-trash:hover { background-color: var(--accent-orange); color: #FFFFFF; }
    .doc-card.trashed { opacity: 0.75; cursor: default; }
    .documents-grid {
      display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 24px;
    }
    .quick-actions-row {
      display: flex; justify-content: center; gap: 24px;
      margin-bottom: 24px; padding-bottom: 24px;
      border-bottom: 1px solid var(--divider-muted);
      transition: border-color 0.4s ease;
    }
    .quick-action-card { width: 220px; flex-shrink: 0; }
    .doc-card {
      background: var(--bg-card); border: 2px solid var(--border-dark); border-radius: 4px;
      height: 240px; padding: 16px; display: flex; flex-direction: column;
      justify-content: space-between; cursor: pointer; position: relative;
      transition: transform 0.15s, background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.15s ease;
    }
    .doc-card:hover { transform: translateY(-4px); box-shadow: 4px 4px 0px var(--shadow-hard); }
    .doc-card-title {
      font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1rem; font-weight: bold;
      color: var(--text-dark); word-break: break-word; line-height: 1.3;
      padding-right: 34px; /* keep long titles from running under the corner badge */
    }
    .doc-card-mla-badge {
      position: absolute; top: 14px; right: 14px;
      /* Trying Bold here per Ethan's request, to see how it reads —
         revert font-weight to 400 (Regular) if he doesn't like it. */
      font-family: 'Courier Prime', 'Courier New', monospace; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.07em;
      /* letter-spacing adds its gap AFTER the last glyph too, which — on
         a right-anchored box — pushes the actual ink further from the
         card's edge than the top offset, breaking the top/right symmetry.
         Pulling the box back in by that same amount keeps the "A"'s ink
         exactly 14px from the edge, same as the top. */
      margin-right: -0.07em;
      color: var(--text-muted); pointer-events: none;
    }
    .doc-card-snippet {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.75rem; color: var(--text-muted);
      overflow: hidden; display: -webkit-box; -webkit-line-clamp: 6;
      -webkit-box-orient: vertical; margin-top: 8px; line-height: 1.4;
    }
    .doc-card-footer {
      display: flex; justify-content: space-between; align-items: center;
      margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--text-muted);
    }
    .doc-card-date { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); }
    .doc-card-actions { display: flex; gap: 8px; align-items: center; }
    .doc-action-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 0.9rem; padding: 2px 4px; }
    .doc-action-btn:hover { color: var(--accent-orange); }

    .create-card { border: 2px dashed var(--border-dark); background: transparent; align-items: center; justify-content: center; gap: 8px; }
    .create-card:hover { background: var(--bg-card); border-style: solid; }
    .create-icon { font-size: 2rem; line-height: 1; color: var(--accent-orange); }
    .create-icon.import-icon svg { fill: var(--accent-orange); }
    .create-text { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-weight: 700; }

    /* EDITOR VIEW */
    .editor-wrapper {
      position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
      background-color: var(--bg-cream); z-index: 100;
      display: flex; flex-direction: row; justify-content: center; align-items: center;
      padding: 40px 20px 20px 20px; transition: padding-right 0.3s cubic-bezier(0.16, 1, 0.3, 1), padding-left 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    }

    .editor-wrapper.thesaurus-open {
      padding-right: 360px;
    }
    .editor-wrapper.write-open {
      padding-right: 520px;
    }
    .editor-wrapper.ref-open {
      padding-left: 380px;
    }
    .editor-wrapper.ref-maximized {
      padding-left: calc(50vw + 20px);
    }
    .editor-wrapper.ref-maximized .btn-thesaurus-bubble,
    .editor-wrapper.ref-maximized .btn-spell-bubble,
    .editor-wrapper.ref-maximized .btn-write-bubble,
    .editor-wrapper.ref-maximized .btn-rhyme-bubble,
    .editor-wrapper.ref-maximized .btn-speak-bubble,
    .editor-wrapper.ref-maximized .btn-align-center,
    .editor-wrapper.ref-maximized .btn-hanging-indent,
    .editor-wrapper.ref-maximized .format-pill,
    .editor-wrapper.ref-maximized .goal-btn-wrapper,
    .editor-wrapper.ref-maximized #editor-read-aloud-stack {
      display: none !important;
    }

    .exit-bubble {
      position: fixed; top: 16px; left: 16px; width: 36px; height: 36px;
      border-radius: 50%; background-color: var(--bg-cream); border: 2px solid var(--border-dark);
      display: flex; align-items: center; justify-content: center;
      cursor: pointer; z-index: 110; transition: 0.15s; box-shadow: 2px 2px 0px var(--border-dark);
      color: var(--text-dark);
    }

    .btn-focus-bubble {
      position: fixed; bottom: 16px; left: 16px; width: 38px; height: 38px;
      background: transparent; border: none; color: var(--text-muted);
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      z-index: 110; transition: color 0.15s;
    }
    .btn-focus-bubble:hover, .btn-focus-bubble.active { color: var(--text-dark); }
    .btn-focus-bubble svg { width: 24px; height: 24px; fill: currentColor; }

    /* FOCUS MODE — fades out every other bubble/pill, leaving just the paper */
    .editor-wrapper.focus-active .btn-thesaurus-bubble,
    .editor-wrapper.focus-active .btn-spell-bubble,
    .editor-wrapper.focus-active .btn-write-bubble,
    .editor-wrapper.focus-active .btn-rhyme-bubble,
    .editor-wrapper.focus-active .btn-speak-bubble,
    .editor-wrapper.focus-active .btn-align-center,
    .editor-wrapper.focus-active .btn-hanging-indent,
    .editor-wrapper.focus-active .btn-mla-drawer,
    .editor-wrapper.focus-active .format-pill,
    .editor-wrapper.focus-active .history-pill,
    .editor-wrapper.focus-active .goal-btn-wrapper,
    .editor-wrapper.focus-active .ref-tab,
    .editor-wrapper.focus-active #editor-read-aloud-stack {
      opacity: 0; pointer-events: none;
    }
    /* status-widget-wrapper is position:fixed (not a .paper-page descendant),
       so it needs its own focus-mode rule rather than riding along with the
       list above. */
    .editor-wrapper.focus-active .status-widget-wrapper {
      opacity: 0; pointer-events: none;
    }

    /* REFERENCE DOCUMENT TAB + PANEL */
    .ref-tab {
      position: absolute; top: 50%; left: -30px; transform: translateY(-50%);
      width: 30px; height: 60px; background: var(--paper-white);
      border: 2px solid var(--border-dark); border-radius: 8px 0 0 8px;
      display: flex; align-items: center; justify-content: center;
      cursor: pointer; z-index: 110; box-shadow: -2px 2px 0px rgba(0,0,0,0.1);
      transition: transform 0.15s, background-color 0.15s, opacity 0.4s ease,
        left 0.3s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s cubic-bezier(0.16, 1, 0.3, 1),
        height 0.3s cubic-bezier(0.16, 1, 0.3, 1), border-radius 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    }
    .ref-tab:hover { transform: translateY(-50%) translateX(-4px); }
    .ref-tab svg { width: 16px; height: 16px; fill: var(--border-dark); transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1), height 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
    .ref-tab.has-file { background: var(--accent-orange); }
    .ref-tab.has-file svg { fill: #FFFFFF; }
    .ref-tab.active svg { transform: rotate(180deg); }

    /* Maximized ref panel pushes #paper-page's left edge to the right;
       the tab shrinks into a small circle centered directly on that edge
       instead of getting cut off under the (now wider) panel. */
    .editor-wrapper.ref-maximized .ref-tab {
      left: -18px; width: 36px; height: 36px; border-radius: 50%;
    }
    .editor-wrapper.ref-maximized .ref-tab svg { width: 17px; height: 17px; }

    .ref-panel {
      position: fixed; top: 0; left: -400px; width: 360px; height: 100vh;
      background-color: var(--paper-white); border-right: 2px solid var(--border-dark);
      z-index: 150; display: flex; flex-direction: column;
      box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.05);
      transition: left 0.3s cubic-bezier(0.16, 1, 0.3, 1), width 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    }
    .ref-panel.mini { left: 0; }
    .ref-panel.maximized { left: 0; width: 50vw; }

    .ref-panel-header {
      display: flex; align-items: center; justify-content: space-between;
      padding: 14px 16px; border-bottom: 2px solid var(--border-dark); gap: 8px; flex-shrink: 0;
    }
    .ref-panel-filename {
      font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.82rem; font-weight: bold;
      color: var(--text-dark); overflow: hidden; text-overflow: ellipsis;
      white-space: nowrap; flex-grow: 1; min-width: 0;
    }
    .ref-panel-actions { display: flex; gap: 6px; flex-shrink: 0; }
    .ref-panel-btn {
      width: 28px; height: 28px; border-radius: 4px; border: 2px solid var(--border-dark);
      background: var(--bg-cream); cursor: pointer; display: flex; align-items: center; justify-content: center;
      color: var(--text-dark); transition: background-color 0.15s, color 0.15s;
    }
    .ref-panel-btn:hover { background-color: var(--border-dark); color: var(--paper-white); }
    .ref-panel-btn svg { width: 13px; height: 13px; fill: currentColor; }

    .ref-panel-body { flex-grow: 1; position: relative; overflow: hidden; }
    .ref-pdf-frame { width: 100%; height: 100%; border: none; display: block; }

    .ref-dropzone {
      height: calc(100% - 28px); display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 14px; padding: 30px; text-align: center; cursor: pointer;
      border: 3px dashed #CCC8C0; margin: 14px; border-radius: 8px;
      transition: border-color 0.15s, background-color 0.15s;
    }
    .ref-dropzone.drag-over { border-color: var(--accent-orange); background-color: rgba(217, 83, 30, 0.06); }
    .ref-dropzone svg { width: 34px; height: 34px; fill: var(--text-muted); }
    .ref-dropzone-text { font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }

    .history-pill {
      position: absolute; top: -50px; left: 0;
      display: flex; flex-direction: row;
      background-color: var(--paper-white);
      border: 2px solid var(--border-dark);
      border-radius: 20px; height: 38px;
      box-shadow: 2px 2px 0px var(--border-dark);
      z-index: 110; overflow: hidden;
      transition: opacity 0.4s ease;
      /* Forces this onto its own GPU compositing layer with pixel-snapped
         edges, so its rounded-corner clip mask is rasterized once and
         cleanly rather than being re-anti-aliased against whatever's
         under it on every paint — belt-and-suspenders alongside the
         children no longer fighting it with their own competing radius
         (see .btn-pill-option's comment). */
      backface-visibility: hidden; transform: translateZ(0);
    }
    .btn-history-pill {
      background: transparent; border: none; outline: none;
      width: 44px; height: 100%; cursor: pointer;
      display: flex; align-items: center; justify-content: center;
      color: var(--border-dark); transition: background-color 0.15s, color 0.15s;
    }
    /* No independent border-radius here — the pill's own rounded corners
       come entirely from .history-pill's border-radius + overflow:hidden
       clip. A second, slightly-mismatched radius on the button itself
       (its curve competing with the parent's own anti-aliased clip mask)
       was the actual source of the hairline background-bleed artifact
       reported on hover — letting one single clip do all the rounding
       removes the seam instead of just hiding it. */
    #undo-btn { border-right: 1px solid var(--border-dark); }
    .btn-history-pill:hover { background-color: var(--border-dark); color: var(--paper-white); }
    /* Same sub-pixel overshoot as the B/I format pill (see its comment):
       a separate, absolutely-positioned fill layer bleeds this segment's
       hover color past the pill's rounded top/bottom/outer-side corners
       so the anti-aliased clip mask never shows a hairline of the
       parent's own background through it, while the shared vertical
       divider (border-right on #undo-btn) stays exactly at 50% since
       this doesn't touch layout at all. 2.5px (up from 0.75px, then
       1.5px, then 1.75px) — a faint arc was still visible along the
       curve at 1.75px. */
    .btn-history-pill { position: relative; }
    .btn-history-pill::before {
      content: ''; position: absolute; top: -2.5px; bottom: -2.5px;
      background-color: inherit; pointer-events: none; z-index: -1;
    }
    #undo-btn::before { left: -2.5px; right: 0; }
    #redo-btn::before { left: 0; right: -2.5px; }
    .btn-history-pill svg { width: 16px; height: 16px; fill: currentColor; }
    /* #FFFFFF, not var(--paper-white) — matches the same convention
       .btn-empty-trash:hover already uses for text on an accent-orange
       fill. --paper-white is a surface/background tone, not guaranteed
       to actually BE white — in dark mode it's a dark card-ish color
       (by design, for inputs/pills), so using it here read as dark
       text sitting on the orange hover fill instead of light. */
    .exit-bubble:hover { background-color: var(--accent-orange); color: #FFFFFF; transform: scale(1.05); }
    .exit-bubble svg { width: 16px; height: 16px; fill: currentColor; }

    /* 0.25s — matches how long the goal bubble itself takes to fade OUT
       in dissolveStatusDisplayTo(), so this button's own reveal lands in
       step with that fade rather than snapping in abruptly sometime
       after it. Opacity/pointer-events rather than the old hard
       display:none specifically so this is actually transitionable —
       same technique .editor-wrapper.focus-active already uses for this
       same element. */
    .goal-btn-wrapper { position: fixed; top: 16px; right: 16px; z-index: 115; opacity: 1; transition: opacity 0.25s ease; }
    .goal-btn-wrapper.hidden { opacity: 0; pointer-events: none; }

    .start-timer-btn {
      background-color: var(--accent-orange); color: #FFFFFF;
      border: 2px solid var(--border-dark); border-radius: 20px;
      padding: 9px 16px 7px; font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
      cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      display: flex; align-items: center; gap: 6px;
      transition: transform 0.15s ease, box-shadow 0.15s ease;
    }
    .start-timer-btn:hover { transform: translateY(-1px); box-shadow: 3px 3px 0px var(--border-dark); }
    .start-timer-btn:active { transform: translateY(0px); box-shadow: 1px 1px 0px var(--border-dark); }


    /* PAPER PAGE
       Format only ever changes at document-creation time (the "Choose Your
       Format" popup) or via bulk Convert in the grid — never while a
       document is open and visible in the editor — so these dimensions are
       set once per document, not animated between states. */
    .paper-page {
      background-color: var(--paper-white);
      width: 100%;
      border: 1px solid #CCC8C0;
      box-shadow: 5px 5px 15px var(--shadow-soft);
      display: flex; flex-direction: column;
      position: relative; margin: 0;
      overflow: visible; transition: transform 0.3s ease;
      transform: scale(var(--zoom-scale, 1));
      transform-origin: top center;
    }
    /* Normal Mode: optimized for on-screen reading/writing comfort, not
       paper fidelity. ~780px keeps the actual text column (after the 60px
       side padding below) in the ~650-680px / 60-75 character sweet spot. */
    .paper-page:not(.mla-mode) {
      max-width: 780px;
      height: calc(100vh - 100px);
    }
    /* MLA Mode: a true 8.5x11 letter page, using the standard 96px-per-inch
       CSS convention (8.5in = 816px, 11in = 1056px). Both width and height
       are computed explicitly from the same --mla-page-h value rather than
       relying on `aspect-ratio` + `height:auto` — that combination isn't
       reliable here because .paper-page is a flex column whose flex-grow
       child (.editor-canvas-container, which contains .editor-canvas's
       padding-bottom:40vh for the typewriter-scroll effect) has an implicit
       min-height:auto, so its content's minimum size wins over the
       aspect-ratio-derived auto height, inflating the page taller than
       intended. Explicit numbers on both axes sidestep that entirely. */
    .paper-page.mla-mode {
      --mla-page-h: min(1056px, calc(100vh - 100px));
      height: var(--mla-page-h);
      width: calc(var(--mla-page-h) * 8.5 / 11);
    }

    /* WORKS CITED MANILA DRAWER — MLA mode only. Bottom-right anchored
       (right:0; bottom:0), width AND height both transition between a
       small "just the tab" closed size and the full editor width/tall-
       panel open size — genuinely resizing this time, not the previous
       round's translateY-reveal-of-a-fixed-full-size-shape approach: that
       version couldn't stop the flat ridge from showing as a thin sliver
       across the FULL width even when "closed", since revealing any
       sliver of a shape that's already full-width reveals a full-width
       sliver. bottom:0 alone (unaffected by width/height ever) is what
       keeps it glued to the editor's bottom edge through the whole
       transition — no separate anchoring logic needed, and nothing to
       "detach". Two separate shape layers crossfade via opacity in sync
       with the resize: .wc-closed-shape and .wc-open-shape are BOTH real
       curved folder silhouettes (not a plain rounded rect for the closed
       one — that read as a generic capsule with no folder-tab identity),
       just drawn against very different box proportions. The closed
       shape's own left edge reuses the exact same step-curve control-
       point ratios as the open shape's shoulder step (the C command
       partway through wc-open-fill's `d`, where control points sit at a
       fixed fraction of the step's horizontal run rather than fixed
       pixel offsets) — but stretched over the CLOSED box's full height
       instead of the open shape's small shoulder-to-tab step, since the
       closed box has no separate flat "shoulder" plateau to show (the
       whole 190x44 box already IS the tab), so the curve itself has to
       carry the descent all the way down to the bottom edge. */
    .works-cited-drawer {
      position: absolute; right: 0; bottom: 0;
      width: 190px; height: 44px;
      transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1), height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
      z-index: 20;
    }
    .works-cited-drawer.active { width: 100%; height: calc(100% - 96px); }
    /* cursor/click-handling sit on the two SHAPE layers specifically, not
       the outer .works-cited-drawer box — that box also contains the
       paper sheet once open, and a click anywhere inside the paper
       (typing, placing the cursor) must never bubble into a toggle-close;
       keeping the pointer cursor and the click listeners scoped to just
       these two elements (see the JS wiring) makes that automatic rather
       than something to filter out afterward. */
    .wc-closed-shape {
      position: absolute; inset: 0; cursor: pointer;
      opacity: 1; transition: opacity 0.4s ease;
    }
    .works-cited-drawer.active .wc-closed-shape { opacity: 0; }
    .wc-closed-shape svg { width: 100%; height: 100%; display: block; overflow: visible; }
    /* The chevron is purely decorative now (see JS — the whole drawer is
       one click target, opening AND closing), so it just needs to sit
       inside whichever shape is actually visible; its own small size
       means it reads fine centered near the top in both states without
       needing separate closed/open positioning. */
    .works-cited-tab-toggle {
      position: absolute; top: 12px; right: calc(5% + 11px); width: 20px; height: 20px;
      color: var(--manila-accent); pointer-events: none; z-index: 1;
    }
    .works-cited-tab-toggle svg {
      width: 100%; height: 100%;
      transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
    }
    .works-cited-drawer.active .works-cited-tab-toggle svg { transform: rotate(180deg); }
    /* Fill and top-contour stroke are two separate paths in the same SVG
       rather than a filled+bordered shape — clip-path clips a border
       around its own full (pre-clip) box too, which meant the fill's own
       1px border was rendering along EVERY edge the path happens to run
       flush with the box on (left/right/bottom, wherever the shape
       doesn't curve away from the box edge) — not just the top contour,
       visibly doubling/thickening the editor's own border right where
       this panel sits flush against it. A stroke-only path that simply
       never traces the left/right/bottom edges at all can't have this
       problem regardless of how the fill and box edges relate.
       preserveAspectRatio="none" stretches both paths to the panel's real
       (non-square) proportions; vector-effect="non-scaling-stroke" on the
       stroke path is what keeps its width a crisp, constant 1.5px on
       screen despite that non-uniform stretch, instead of the stroke
       itself stretching unevenly thick/thin along the curve. */
    .wc-open-shape {
      position: absolute; inset: 0; cursor: pointer;
      opacity: 0; transition: opacity 0.4s ease;
    }
    .works-cited-drawer.active .wc-open-shape { opacity: 1; }
    .wc-open-shape svg { width: 100%; height: 100%; display: block; overflow: visible; }
    .wc-open-fill, .wc-closed-fill { fill: var(--manila-primary); }
    .wc-open-stroke, .wc-closed-stroke { fill: none; stroke: var(--manila-accent); stroke-width: 1.5; }
    /* The paper sheet itself — sharp 90-degree corners (unlike every
       rounded corner elsewhere in this app's UI chrome) specifically to
       read as a physical sheet of paper tucked inside the folder, same
       --paper-white the regular editor canvas uses. A real fixed manila-
       frame gap on three sides (left/right/bottom) plus enough clearance
       above to sit entirely below the shoulder curve — a sibling of both
       shape layers (not a child of either), so it's structurally
       incapable of ever being clipped/cut through by them. The 96px
       padding here is a full MLA-standard 1-inch margin (96px at this
       app's existing 96px-per-inch convention — see .paper-page.mla-mode)
       for the actual Works Cited text, entirely separate from the small
       outer manila-frame gap around the sheet itself. Only visible once
       .active (opacity, not display, so it can fade in smoothly) — its
       own left/right/bottom/top values are nonsense against the tiny
       closed-size box they'd otherwise resolve against, but that never
       matters since it's invisible and unclickable until then anyway. */
    .works-cited-paper {
      position: absolute; left: 24px; right: 24px; bottom: 24px; top: 64px;
      background: var(--paper-white);
      padding: 96px; box-sizing: border-box;
      overflow: hidden; opacity: 0; pointer-events: none;
      transition: opacity 0.25s ease;
      display: flex; flex-direction: column;
    }
    .works-cited-drawer.active .works-cited-paper { opacity: 1; pointer-events: auto; }
    .works-cited-header {
      text-align: center; font-family: 'Tinos', 'Times New Roman', Times, serif; font-size: 12pt; line-height: 2;
      color: var(--text-dark); margin-bottom: 8px; flex-shrink: 0; user-select: none;
    }
    .works-cited-body {
      flex: 1; min-height: 0; overflow-y: auto;
      font-family: 'Tinos', 'Times New Roman', Times, serif; font-size: 12pt; line-height: 2;
      color: var(--text-dark); outline: none;
    }
    /* Every entry gets the hanging indent automatically — unlike the main
       editor's mla-hanging-indent (a toggle for occasional use), a Works
       Cited page has no other kind of paragraph, so this applies to every
       div/p a citation ends up wrapped in without needing a button press. */
    .works-cited-body div, .works-cited-body p { text-indent: -0.5in; margin-left: 0.5in; }

    /* FORMATTING PILL */
    .format-pill {
      position: absolute;
      top: 0; right: -48px;
      display: flex; flex-direction: column;
      background-color: var(--paper-white);
      border: 2px solid var(--border-dark);
      border-radius: 20px; width: 38px;
      box-shadow: 2px 2px 0px var(--border-dark);
      z-index: 110; overflow: hidden;
      transition: opacity 0.4s ease;
      /* Same GPU-layer/pixel-snap treatment as .history-pill — see its
         comment. */
      backface-visibility: hidden; transform: translateZ(0);
    }
    .btn-align-center {
      position: absolute;
      top: 92px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--paper-white);
      border: 2px solid var(--border-dark); color: var(--text-dark);
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: background-color 0.15s, color 0.15s, opacity 0.4s ease;
    }
    .btn-align-center svg { width: 16px; height: 16px; fill: currentColor; }
    .btn-align-center:hover, .btn-align-center.active {
      background-color: var(--border-dark); color: var(--paper-white);
    }
    .btn-hanging-indent {
      position: absolute;
      top: 142px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--paper-white);
      border: 2px solid var(--border-dark); color: var(--text-dark);
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: background-color 0.15s, color 0.15s, opacity 0.4s ease;
    }
    .btn-hanging-indent svg { width: 16px; height: 16px; fill: currentColor; }
    .btn-hanging-indent:hover, .btn-hanging-indent.active {
      background-color: var(--border-dark); color: var(--paper-white);
    }
    /* Overrides MLA's default first-line indent for a specific paragraph —
       used for Works Cited entries, where the first line sits flush and
       everything after it indents instead. */
    .editor-canvas.mla-mode > div.mla-hanging-indent,
    .editor-canvas.mla-mode > p.mla-hanging-indent {
      text-indent: -0.5in; margin-left: 0.5in;
    }

    .btn-pill-option {
      background: transparent; border: none; outline: none;
      width: 100%; height: 38px; cursor: pointer;
      display: flex; align-items: center; justify-content: center;
      transition: background-color 0.15s, color 0.15s;
    }
    /* Neither pill option carries its own border-radius — see the
       matching comment on #undo-btn/#redo-btn above; .format-pill's own
       overflow:hidden clip does all the corner rounding for both. */
    /* Vertical nudges live on .btn-pill-option-glyph (the inner <span>),
       never on the button itself — the button's own box is what the
       active/hover background fill paints against, flush with
       .format-pill's rounded top/bottom clip. Nudging the BUTTON down
       used to pull that fill down with it, opening a sliver gap between
       the fill and the pill's actual top border. Nudging just the glyph
       leaves the fill perfectly anchored and only moves the character. */
    .btn-pill-option.bold, .btn-pill-option.italic {
      /* Containing block for the ::before overshoot layer below — doesn't
         itself move or resize the button (no top/left offsets given), so
         flex layout and the interior seam position are untouched. */
      position: relative;
    }
    /* The active/hover fill is painted via background-color on the
       button itself, which is exactly sized to its flex-laid-out box —
       flush at the OUTER pill border, but a plain rectangle can leave a
       sub-pixel anti-aliasing hairline right at .format-pill's rounded
       corners, where its own clip mask curves away from that flat edge
       faster than the rectangle does. A margin-based overshoot would fix
       that but ALSO shrinks this item's flex-used size, nudging the
       straight seam against the other segment off of exact 50% — so
       instead this is a separate, absolutely-positioned (out of flow,
       zero layout impact) fill layer that can overshoot the OUTER
       corners only, leaving the shared seam alone. background-color:
       inherit tracks the real button's hover/active color live. */
    /* 2.5px (up from 0.75px, then 1.5px, then 1.75px) — a faint arc was
       still visible along the bottom curved edge at 1.75px. */
    .btn-pill-option.bold::before, .btn-pill-option.italic::before {
      content: ''; position: absolute; left: -2.5px; right: -2.5px;
      background-color: inherit; pointer-events: none; z-index: -1;
    }
    .btn-pill-option.bold::before { top: -2.5px; bottom: 0; }
    .btn-pill-option.italic::before { top: 0; bottom: -2.5px; }
    .btn-pill-option.bold {
      color: var(--border-dark); font-weight: 700;
      font-family: 'Courier Prime', 'Courier New', monospace;
      font-size: 1.34rem; border-bottom: 1px solid var(--border-dark);
    }
    .btn-pill-option.bold:hover, .btn-pill-option.bold.active {
      background-color: var(--border-dark); color: var(--paper-white);
    }
    .btn-pill-option.italic {
      color: var(--border-dark); font-style: italic; font-weight: normal;
      font-family: 'Courier Prime', 'Courier New', monospace; font-size: 1.4rem;
    }
    /* Only "B" gets nudged — "I"'s centering was already correct, and
       nudging it too would just push it back out of alignment again. */
    .btn-pill-option.bold .btn-pill-option-glyph {
      display: inline-block; line-height: 1; transform: translateY(2.5px);
    }
    .btn-pill-option.italic .btn-pill-option-glyph {
      display: inline-block; line-height: 1;
    }
    .btn-pill-option.italic:hover, .btn-pill-option.italic.active {
      background-color: var(--border-dark); color: var(--paper-white);
    }

    /* THESAURUS BUBBLE */
    .btn-thesaurus-bubble {
      position: absolute; top: 148px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--thesaurus-blue);
      border: 2px solid var(--border-dark); color: #FFFFFF;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: transform 0.15s, background-color 0.15s, opacity 0.4s ease;
    }
    .btn-thesaurus-bubble svg { width: 18px; height: 18px; fill: currentColor; }
    .btn-thesaurus-bubble:hover, .btn-thesaurus-bubble.active {
      transform: scale(1.08); background-color: var(--thesaurus-blue-dark); will-change: transform;
    }

    .btn-spell-bubble {
      position: absolute; top: 200px; right: -48px;
      width: 38px; height: 38px;
      color: #FFFFFF;
      font-family: 'Courier Prime', 'Courier New', monospace; font-weight: bold; font-size: 1.45rem;
      cursor: pointer; z-index: 110;
      transition: opacity 0.4s ease;
      /* Every other bubble button still carries its own background/
         border/border-radius directly, which happens to be enough to
         paint over the browser's native <button> chrome. This one no
         longer does (that's all moved to .btn-spell-bubble-shell below)
         so without an explicit reset here, the button fell back to
         its native OS widget skin — a light gray rounded-rect with its
         own outset border, showing through as a mask around the shell.
         That native skin also imposes its own internal inset, which is
         why the shell (sized via inset:0 against this button's padding
         box) was rendering 4px smaller than every other bubble's own
         38px circle. appearance:none removes the widget rendering
         entirely, leaving a plain, fully transparent, zero-padding box
         — exactly the invisible click target this button is meant to
         be now that the shell handles all the visible paint. */
      appearance: none; -webkit-appearance: none; background: none; border: none;
      /* "?" is the only plain text glyph among these bubbles (the rest
         use SVG icons, which aren't selectable text to begin with) —
         clicking rapidly nearby can register as a double-click-style
         select gesture and highlight it like normal text. It's meant to
         read as part of the bubble's fixed artwork, not selectable
         content, so block selection entirely. */
      user-select: none; -webkit-user-select: none;
    }
    /* Circle background/border/shadow live on this absolutely-stacked
       sibling of the glyph (not the button itself) so the hover
       scale-up below only ever touches this shell — same shell/glyph
       split used for the Build-an-Essay D-pad nodes, and for the same
       reason: a transform on an ancestor of the glyph would drag the
       glyph's own text box along with it. Previously the button itself
       both scaled AND held the text, and since the glyph's optically-
       nudged center (see .btn-spell-bubble-glyph below) sits a couple
       px off the button's true geometric center, scaling from that
       off-center point on hover visibly dragged the "?" down/left by a
       fraction of a pixel — small, but exactly the "budging" this
       fixes structurally rather than by chasing the right magic-number
       transform-origin. */
    .btn-spell-bubble-shell {
      position: absolute; inset: 0; border-radius: 50%;
      background-color: var(--spell-teal); border: 2px solid var(--border-dark);
      box-shadow: 2px 2px 0px var(--border-dark);
      transition: transform 0.15s, background-color 0.15s;
    }
    .btn-spell-bubble:hover .btn-spell-bubble-shell, .btn-spell-bubble.active .btn-spell-bubble-shell {
      transform: scale(1.08); background-color: var(--spell-teal-dark); will-change: transform;
    }
    /* Centered directly against the button's own (never-transformed) box
       rather than via the old flex centering, so it's fully independent
       of the shell. translateY(2px) is the same optical nudge as
       before — align-items:center-style centering would otherwise
       center the "?" against its full em-box, which includes descender
       space the glyph itself doesn't use, reading as sitting too high. */
    .btn-spell-bubble-glyph {
      position: absolute; top: 50%; left: 50%; line-height: 1;
      transform: translate(-50%, -50%) translateY(2px);
    }

    .btn-write-bubble {
      position: absolute; top: 252px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--write-burgundy);
      border: 2px solid var(--border-dark); color: #FFFFFF;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: transform 0.15s, background-color 0.15s, opacity 0.4s ease;
    }
    .btn-write-bubble svg { width: 17px; height: 17px; fill: currentColor; }
    .btn-write-bubble:hover, .btn-write-bubble.active {
      transform: scale(1.08); background-color: var(--write-burgundy-dark); will-change: transform;
    }

    .btn-rhyme-bubble {
      position: absolute; top: 304px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--rhyme-purple);
      border: 2px solid var(--border-dark); color: #FFFFFF;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: transform 0.15s, background-color 0.15s, opacity 0.4s ease;
    }
    /* Raster (not the SVG every sibling bubble icon uses) — this is
       Ethan's own reference photo/sketch of a microphone. Source was a
       black line-art mic+cord outline on transparent background; every
       sibling bubble icon here is a solid filled silhouette, not an
       outline, so a plain recolor (white line art) still read as
       inconsistent next to them — flood-filled the mic head/handle's own
       enclosed outline into solid white instead (the cord stays a thin
       line since it's open, not a closed shape, same as it would with a
       real SVG fill). Cropped tighter than the first pass specifically
       to chop more of the cord's tail off (rather than shrinking the
       whole image to fit it in — a bigger mic reads better in a 38px
       circle than a smaller mic plus more intact cord), with a few extra
       pixels of top padding baked into the source crop itself so the
       mic sits shifted down and genuinely centered in the button instead
       of optically high. Embedded at ~5x its display size (126px source
       down to a 24px box) so it stays crisp on high-DPI screens instead
       of just barely clearing 1x — object-fit:contain keeps its own
       aspect ratio if the button's box ever changes. */
    .btn-rhyme-bubble img { width: 24px; height: 24px; object-fit: contain; display: block; }
    .btn-rhyme-bubble:hover, .btn-rhyme-bubble.active {
      transform: scale(1.08); background-color: var(--rhyme-purple-dark); will-change: transform;
    }

    .btn-speak-bubble {
      position: absolute; top: 356px; right: -48px;
      width: 38px; height: 38px; border-radius: 50%;
      background-color: var(--speak-mustard);
      border: 2px solid var(--border-dark); color: #FFFFFF;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      box-shadow: 2px 2px 0px var(--border-dark); z-index: 110;
      transition: transform 0.15s, background-color 0.15s, opacity 0.4s ease;
    }
    .btn-speak-bubble svg { width: 18px; height: 18px; fill: currentColor; }
    .btn-speak-bubble:hover, .btn-speak-bubble.active {
      transform: scale(1.08); background-color: var(--speak-mustard-dark); will-change: transform;
    }

    /* 8-BIT ARCADE ACTION BUBBLES — Build-an-Essay only. Scoped with
       .workshop-wrapper (not body.workshop-mode, unlike the palette
       above) because these five buttons are genuine DOM descendants of
       #workshop-view — separate elements from the Document Center's own
       bubbles, not shared singletons — so the plain, local descendant
       selector is the more precise tool here. Square borders + a hard
       offset shadow replace the Document Center's circle + soft 2px
       shadow; hover swaps in a flashing arcade selection outline on top
       of the existing scale+brighten instead of just the darken the
       Document Center uses. */
    .workshop-wrapper .btn-thesaurus-bubble,
    .workshop-wrapper .btn-write-bubble,
    .workshop-wrapper .btn-rhyme-bubble,
    .workshop-wrapper .btn-speak-bubble {
      /* position:relative (this selector's two classes beat
         .essay-action-dock-btn's one on specificity, so this wins
         despite .essay-action-dock-btn setting `static`) turns each
         button into a real positioning context for its own ::after
         selection frame below — needed because that frame's offsets
         differ per side, which plain `outline` can't express. */
      position: relative;
      border-radius: 0; border: 2px solid #000000; box-shadow: 4px 4px 0px #000000;
    }
    /* .btn-spell-bubble's own circle chrome now lives on its -shell
       sibling (see the Document Center rule above), so this square-
       border/hard-shadow override targets the shell too, not the
       button itself. */
    .workshop-wrapper .btn-spell-bubble-shell {
      border-radius: 0; border: 2px solid #000000; box-shadow: 4px 4px 0px #000000;
    }
    /* PURPLE "TRANSITIONS & CITATIONS" DOCK BUTTON — Stage 5 exclusive.
       Unlike the four above, this one has no non-workshop/editor-side
       counterpart to inherit a base size/background from (it only ever
       exists in the workshop), so everything lives in one self-contained
       rule instead of being layered editor-base + workshop-override. */
    .workshop-wrapper .btn-transitions-bubble {
      position: relative; width: 38px; height: 38px;
      display: flex; align-items: center; justify-content: center; cursor: pointer;
      background-color: #7B2CBF; border-radius: 0; border: 2px solid #000000; box-shadow: 4px 4px 0px #000000;
      color: #FFFFFF; transition: background-color 0.15s, transform 0.15s;
    }
    .workshop-wrapper .btn-transitions-bubble svg { width: 18px; height: 18px; fill: currentColor; }
    /* Same hover/active treatment (scale + darken) the other four dock
       buttons already get, inherited there from their editor-side base
       rule (e.g. .btn-thesaurus-bubble:hover) — spelled out explicitly
       here since there's no such base rule for this button to inherit
       it from. */
    .workshop-wrapper .btn-transitions-bubble:hover, .workshop-wrapper .btn-transitions-bubble.active {
      background-color: #9D4EDD; transform: scale(1.08); will-change: transform;
    }
    /* The yellow "selected" frame used to be a plain `outline`, offset
       4px on all sides via outline-offset — symmetric by construction,
       even though the shape it's framing isn't. Each button's hard
       drop shadow (box-shadow: 4px 4px 0px #000, no blur/spread) only
       extends the visible silhouette 4px to the right and 4px down,
       never up or left. A uniform outline-offset left a real 4px gap
       of bare background on the top/left (nothing there to clear) while
       landing at a mathematically ZERO gap against the shadow's own
       edge on the right/bottom — flush at best, one sub-pixel rounding
       error from visibly overlapping it, which is exactly the
       "shadow clipping the frame" that got reported, alongside the
       top/left gap reading as oversized by comparison. `outline` can
       only take one offset for all four sides; a ::after sized via
       per-side inset values can frame the TOTAL silhouette — button
       plus shadow — with a real, equal 4px gap on every side instead. */
    .workshop-wrapper .btn-thesaurus-bubble::after,
    .workshop-wrapper .btn-write-bubble::after,
    .workshop-wrapper .btn-rhyme-bubble::after,
    .workshop-wrapper .btn-speak-bubble::after,
    .workshop-wrapper .btn-spell-bubble-shell::after {
      content: ''; position: absolute; pointer-events: none;
      /* 6px real gap between the frame and the total silhouette (button
         + shadow) on every side — per Ethan, 1px down from 7px.
         top/left: 6px clear of the button's own edge (no shadow to
         clear there). right/bottom: 6px clear of the SHADOW's edge,
         which already sits 4px past the button's own edge on its own —
         so 10px off the button's own box, landing the same real 6px
         gap past whatever is actually visible on that side. */
      top: -6px; left: -6px; right: -10px; bottom: -10px;
      border: 2px solid transparent;
    }
    .workshop-wrapper .btn-thesaurus-bubble:hover::after, .workshop-wrapper .btn-thesaurus-bubble.active::after,
    .workshop-wrapper .btn-write-bubble:hover::after, .workshop-wrapper .btn-write-bubble.active::after,
    .workshop-wrapper .btn-rhyme-bubble:hover::after, .workshop-wrapper .btn-rhyme-bubble.active::after,
    .workshop-wrapper .btn-speak-bubble:hover::after, .workshop-wrapper .btn-speak-bubble.active::after {
      animation: arcade-bubble-select 0.6s steps(1) infinite;
    }
    /* .btn-spell-bubble split out from the group above because its own
       box+shadow live on a separate -shell CHILD (see the D-PAD
       SHELL/NUMBER SPLIT comment), not on the button itself — hover/
       active still toggle on the outer button, so the frame has to be
       triggered from there but rendered on the shell's own ::after. */
    .workshop-wrapper .btn-spell-bubble:hover .btn-spell-bubble-shell::after,
    .workshop-wrapper .btn-spell-bubble.active .btn-spell-bubble-shell::after {
      animation: arcade-bubble-select 0.6s steps(1) infinite;
    }
    @keyframes arcade-bubble-select {
      0%, 49% { border-color: #FFC107; }
      50%, 100% { border-color: transparent; }
    }
    /* Same yellow selection frame as the other four dock buttons, per
       Ethan — purple is this button's own idle/hover fill color (see
       .btn-transitions-bubble above), but the "you're here" selection
       frame stays the one consistent color the whole column already
       uses, not a second accent competing with it. */
    .workshop-wrapper .btn-transitions-bubble::after {
      content: ''; position: absolute; pointer-events: none;
      top: -6px; left: -6px; right: -10px; bottom: -10px;
      border: 2px solid transparent;
    }
    .workshop-wrapper .btn-transitions-bubble:hover::after, .workshop-wrapper .btn-transitions-bubble.active::after {
      animation: arcade-bubble-select 0.6s steps(1) infinite;
    }
    /* "Did You Mean?"'s bubble is just a bare "?" character (no SVG icon
       like the other four) — Press Start 2P turns that lone glyph into
       an 8-bit sprite on its own, no artwork needed. */
    .workshop-wrapper .btn-spell-bubble { font-family: 'Press Start 2P', monospace; font-size: 0.8rem; }

    /* READ ALOUD FACE + AUDIO CONTROLS — a small pixel-art character that
       mirrors the bubble column onto the opposite edge while a read-aloud
       session is active. Colors are fixed (not theme-variable) on purpose:
       this is a little mascot with its own constant identity, not
       structural chrome that should invert in dark mode. */
    .read-aloud-stack {
      display: flex; flex-direction: column; align-items: center; gap: 14px;
      transition: opacity 0.4s ease;
    }
    /* top: 92px matches .btn-align-center's top exactly (not the
       thesaurus bubble's 148px) — deliberately aligns the face with the
       center-align control on the opposite edge so it reads as a level
       pair instead of crowding down near the reference tab. */
    #editor-read-aloud-stack { position: absolute; left: -48px; top: 92px; z-index: 110; }
    #workshop-read-aloud-stack { position: fixed; z-index: 110; transition: top 0.08s linear, opacity 0.4s ease; }
    .read-aloud-face { width: 38px; height: 38px; cursor: pointer; }
    .read-aloud-face svg { width: 100%; height: 100%; display: block; transform: scaleX(-1); }
    /* var(--border-dark), not hardcoded #222222 — these never adapted to
       dark mode at all before, rendering as near-black outline/eyes/
       mouth against an also-dark page (barely visible). --border-dark
       is deliberately kept tied to the bright/legible ink color in both
       dark palettes (Document Center's warm parchment and the
       workshop's cool phosphor white — see their own variable blocks),
       so this automatically becomes "whatever's brightest in the
       current mode" and correctly differs between the two contexts,
       while staying an exact match to the old #222222 in light mode
       (border-dark's own light-mode value). .face-fill stays on
       --paper-white deliberately — it was always meant to read as a
       subtle body shape rather than provide the actual visibility
       (that's the outline/eyes/mouth's job), the same low-key
       relationship it already had against the light-mode page. */
    .face-edge rect, .face-eyes rect { fill: var(--border-dark); }
    .face-fill rect { fill: var(--paper-white); }
    .face-mouth rect { fill: var(--border-dark); }
    /* SVG shapes only register clicks on their own painted pixels by
       default (pointer-events: visiblePainted) — the small, per-frame
       eyes/mouth rects redraw at different sizes on every talk-loop tick,
       so a click aimed at "the mouth" can land a pixel off, in a gap the
       current frame's shape doesn't happen to cover, and fall through to
       nothing. Turning off hit-testing on just these two dynamic groups
       means every click within the face's visible circle always lands on
       the static face-fill/face-edge layers underneath (or the
       .read-aloud-face div itself) instead, which bubbles to the same
       boop handler regardless of exactly which pixel was clicked. */
    .face-eyes, .face-mouth { pointer-events: none; }
    /* Frameless — standalone clickable icons, no bubble/circle chrome
       underneath. The button itself stays a full 38px hit target
       (box-sizing: border-box keeps padding from inflating it) even
       though the icon glyph inside renders smaller than that. */
    .read-aloud-ctrl-btn {
      width: 38px; height: 38px; box-sizing: border-box; padding: 8px;
      background: transparent; border: none; box-shadow: none;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      z-index: 110;
      transition: transform 0.15s, opacity 0.15s;
    }
    .read-aloud-ctrl-btn:hover { transform: scale(1.12); opacity: 0.68; }
    .read-aloud-ctrl-btn svg { width: 100%; height: 100%; }
    /* Same fix/reasoning as .face-edge etc. above — pause/restart/close
       were also hardcoded #222222, invisible against a dark page. */
    .read-aloud-ctrl-btn rect { fill: var(--border-dark); }

    /* EDITOR CANVAS */
    .editor-canvas-container {
      flex-grow: 1; overflow-y: auto; position: relative;
      padding: 56px 60px 40px 60px;
      scrollbar-width: thin; scrollbar-color: rgba(34, 34, 34, 0.25) transparent;
      scroll-behavior: smooth;
    }
    .doc-title-input {
      font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1.8rem; font-weight: bold;
      border: none; outline: none; background: transparent; color: var(--text-dark);
      /* Title-to-body gap tightened to ~1.35x the editor's own paragraph
         spacing (.editor-canvas p's 1.25em / 20px), down from ~3x before.
         padding-bottom(12) + margin-bottom(5) here, plus .editor-canvas's
         own 10px top padding (a deliberate margin-collapse guard — see
         its comment, left untouched), totals 27px vs. 20px of standard
         paragraph spacing. */
      padding: 0 0 12px 0; width: 100%; margin-bottom: 5px; text-align: center;
      resize: none; overflow: hidden; line-height: 1.25; display: block;
      filter: blur(0px); transition: filter 0.2s ease;
    }
    .doc-title-input.blurred-for-drop { filter: blur(3px); }
    .doc-title-input::placeholder { color: #BBB8B2; }
    .doc-title-input.title-placeholder-active { color: var(--text-muted); }

    .editor-canvas-container::-webkit-scrollbar { width: 6px; }
    .editor-canvas-container::-webkit-scrollbar-track { background: transparent; }
    .editor-canvas-container::-webkit-scrollbar-thumb {
      background-color: rgba(34, 34, 34, 0.18); border-radius: 4px;
    }
    .editor-canvas-container::-webkit-scrollbar-thumb:hover { background-color: rgba(34, 34, 34, 0.4); }

    /* DOCUMENT OUTLINE NUBS — PAUSED (commented out, not deleted)
       Built and working (minimap-style ticks in the left gutter, proportional
       positioning, active-state extension while a heading is in view, click-
       to-jump), but paused per user feedback: the track stays fixed in place
       through a scroll rather than moving/reacting in a way that reads as
       responsive, which felt visually static/disruptive. Revisit the
       animation approach before re-enabling. To restore: uncomment this CSS
       block, the doc-outline-track element in the HTML, and the
       corresponding JS block (search "DOCUMENT OUTLINE NUBS" in <script>).

    A lightweight "minimap" of the document's H1/H2/H3 headings, living in
    the editor's left-hand gutter. Sits as a sibling of editor-canvas-
    container so it's positioned relative to .paper-page (the nearest
    non-scrolling positioned ancestor) instead of the scrolling container
    itself — that keeps the track static while the document scrolls under
    it, the same way a real scrollbar track stays put while its thumb
    moves. Each nub's vertical position is computed from the heading's
    offsetTop as a fraction of the container's scrollHeight, mapped onto
    the container's clientHeight — the standard scrollbar-thumb mapping,
    so a nub's position always matches where that heading currently sits
    along the scrollable range.
    .doc-outline-track {
      position: absolute; top: 0; bottom: 0; left: 18px; width: 1px;
      pointer-events: none; z-index: 95;
      transition: opacity 0.4s ease;
    }
    .editor-wrapper.focus-active .doc-outline-track {
      opacity: 0; pointer-events: none;
    }
    .doc-outline-nub {
      position: absolute; left: 0; height: 2px; border-radius: 1px;
      background-color: var(--accent-orange); opacity: 0.35;
      pointer-events: auto; cursor: pointer;
      transition: width 0.3s ease, opacity 0.3s ease;
    }
    .doc-outline-nub[data-level="1"] { width: 13px; }
    .doc-outline-nub[data-level="2"] { width: 9px; margin-left: 4px; }
    .doc-outline-nub[data-level="3"] { width: 6px; margin-left: 7px; }
    "Passed through" state — while the visible scroll range contains this
    heading, the nub extends outward a little. Deliberately subtle: a
    width change plus an opacity bump, no motion/scale/color shift.
    .doc-outline-nub.nub-active { opacity: 0.9; }
    .doc-outline-nub[data-level="1"].nub-active { width: 21px; }
    .doc-outline-nub[data-level="2"].nub-active { width: 16px; }
    .doc-outline-nub[data-level="3"].nub-active { width: 12px; }
    */

    .editor-canvas {
      font-family: 'Georgia', serif; font-size: 16px; line-height: 1.5;
      color: var(--text-dark); outline: none; min-height: 80%;
      /* Small but deliberate — with zero top padding, a first-child
         block's own margin-top can collapse straight through this
         element and merge with whatever spacing exists above it
         (the title), rather than adding real space inside the editor.
         Confirmed directly: a dropped image as the very first thing in
         a document only had ~16px of actual clearance from the title
         above it (exactly its own 1em margin, nothing extra) — tight
         enough that the image's own selection outline could plausibly
         reach the title text. This padding forces the margin to render
         fully inside the editor instead, on top of whatever margin the
         first block already has, so there's real, guaranteed clearance
         regardless of margin-collapse specifics. Negligible for
         text-only documents (less than a single line-height), but a
         real safety margin for anything that opens with an image.
         */
      padding-top: 10px;
      padding-bottom: 40vh;
      text-align: left; hyphens: auto; -webkit-hyphens: auto; -moz-hyphens: auto;
      filter: blur(0px); transition: filter 0.2s ease;
    }
    /* A gentle blur on the actual document text while a file is being
       dragged over the editor — draws focus toward the drop overlay
       sitting on top of it (a separate, sibling element, so it's
       unaffected by this and stays perfectly sharp) rather than the
       words underneath. */
    .editor-canvas.blurred-for-drop { filter: blur(3px); }
    /* Explicit paragraph rhythm instead of relying on double line breaks —
       Normal Mode only; MLA resets this to 0 below to keep its continuous,
       indent-separated double-spaced flow intact. */
    .editor-canvas p, .editor-canvas div { margin-bottom: 1.25em; }
    /* Everything except headings/blockquotes gets forced back to plain body
       styling (this is what keeps pasted content from carrying in random
       fonts/sizes/colors — see the Clean Paste sanitizer). Headings and
       blockquotes are excluded so the markdown-shortcut sizing below can
       actually take effect; they still get font-family/color forced for
       dark-mode consistency and Clean Paste safety, just not font-size. */
    .editor-canvas *:not(h1):not(h2):not(h3):not(blockquote) {
      font-family: inherit !important; font-size: inherit !important;
      color: inherit !important; background-color: transparent !important;
    }
    .editor-canvas h1, .editor-canvas h2, .editor-canvas h3 {
      font-family: inherit !important; color: inherit !important; background-color: transparent !important;
      font-weight: bold; margin: 0.6em 0 0.3em 0;
    }
    /* A heading typed as the very first line would otherwise add its own
       0.6em margin-top on top of the title's already-tightened spacing
       above (.editor-canvas's own padding-top guards against collapse,
       so that margin renders in full, not collapsed away) — right back
       to the awkward extra whitespace this was just fixed for, just for
       documents that happen to open with a heading instead of a plain
       paragraph. The editor wraps every top-level line in its own <div>
       (see the .editor-canvas padding-top comment above), so the actual
       first heading sits one level deeper than .editor-canvas's own
       direct first child — both depths are covered here. */
    .editor-canvas > :first-child,
    .editor-canvas > :first-child > :first-child { margin-top: 0; }
    .editor-canvas h1 { font-size: 1.6em; }
    .editor-canvas h2 { font-size: 1.35em; }
    .editor-canvas h3 { font-size: 1.15em; }
    .editor-canvas blockquote {
      font-family: inherit !important; color: var(--text-muted) !important; background-color: transparent !important;
      font-style: italic; border-left: 3px solid var(--text-muted);
      margin: 0.6em 0; padding: 0.1em 0 0.1em 16px;
    }
    .editor-canvas hr { border: none; border-top: 1px solid var(--text-muted); margin: 1em 0; }

    /* DROPPED IMAGES — center-aligned block, corner-drag resizable.
       contenteditable="false" on the wrapper makes it "atomic" inside the
       editable region: the cursor can land immediately before or after
       it, arrow-key/click navigation treats it as a single unit, and
       none of the editor's text-editing logic (markdown shortcuts,
       Clean Paste's tag handling, etc.) ever tries to reach inside it. */
    .editor-image-wrap {
      display: block; position: relative; margin: 1em auto; line-height: 0;
      /* fit-content, not the default auto — a block-level div with no
         explicit width just stretches to fill its parent, which gives
         margin:auto nothing to actually center (there's no leftover
         space to distribute). Confirmed directly: without this,
         resizing the image never recentered it horizontally at all,
         it just stayed pinned to wherever its left edge happened to be. */
      width: fit-content;
      /* max-width matches the usable text column itself (the canvas's
         own rendered width already excludes the container's 60px side
         padding) minus a generous inset — the outline shown when an
         image is selected extends a few px past the image's own edge
         (outline-offset + outline-width), so this needs real clearance,
         not just a token few px, to genuinely never reach past where
         the title/body text itself can go. */
      max-width: calc(100% - 24px);
    }
    .editor-image-wrap img {
      display: block; width: 340px; max-width: 100%; height: auto;
      border-radius: 2px; cursor: grab;
    }
    /* Thin orange selection indicator — same accent color and a similar
       thin-line feel to the resize handle's own strokes, appears the
       instant you press the image (no release needed) and stays until
       you click anywhere outside it. */
    .editor-image-wrap.selected img { outline: 2px solid var(--accent-orange); outline-offset: 3px; }
    .editor-image-wrap.being-dragged { opacity: 0.8; }
    .editor-image-wrap.being-dragged img { cursor: grabbing; }
    /* Invisible until the image itself is hovered — never visible just
       from looking at the document. */
    .editor-image-resize-handle {
      position: absolute; right: -15px; width: 30px; height: 30px;
      /* top:calc(100% - 15px), not bottom:-15px — despite being
         mathematically equivalent, `bottom` was resolving against an
         effectively shorter containing-block height than `right` was
         (confirmed directly: bottom:-15px measured 20px too high,
         consistently, regardless of the exact value used — a real
         browser quirk here, not explained by any padding/border/margin/
         transform on the wrap or its ancestors). Switching the vertical
         axis to `top` with an explicit calc() sidesteps whatever that
         quirk was and lands exactly on the expected pixel. */
      top: calc(100% - 15px);
      /* Centered exactly on the image's corner point — right/top-calc
         are both anchored 15px in from the true corner (half the box's
         own 30px width/height), so the same amount extends past the
         edge as sits inside it, symmetric in both directions. */
      display: flex; align-items: center; justify-content: center;
      cursor: nwse-resize; opacity: 0; transition: opacity 0.15s ease, color 0.15s ease;
      /* !important because .editor-canvas *:not(...) forces
         color:inherit !important on every descendant (the Clean Paste
         sanitizer) — without matching it, this and the .dragging color
         below would silently lose to that rule regardless of selector
         specificity. Confirmed directly: without !important the handle
         rendered in plain text-dark even while actively dragging. */
      color: var(--text-dark) !important; pointer-events: none;
    }
    .editor-image-wrap:hover .editor-image-resize-handle { opacity: 0.55; pointer-events: auto; }
    .editor-image-resize-handle:hover { opacity: 0.85; }
    /* Same idea as Build-an-Essay's textarea resize handle turning blue
       mid-drag — this one turns the program's accent orange instead,
       and stays fully opaque for the duration of the drag regardless of
       whether the cursor is still technically over the handle itself.
       Selecting the image on its own does NOT turn this orange — per
       Ethan's follow-up, it should stay transparent right up until an
       actual drag/interaction starts, not just from being selected. */
    .editor-image-resize-handle.dragging {
      opacity: 1 !important; color: var(--accent-orange) !important;
    }
    .editor-image-resize-handle svg { width: 17px; height: 17px; }

    /* DRAG-OVER OVERLAY — shown across the whole editor while an image
       file is being dragged over it, hidden the rest of the time. Takes
       on the program's orange accent throughout, per spec. */
    .editor-drop-overlay {
      position: absolute; inset: 0; z-index: 50;
      background: rgba(217, 83, 30, 0.08);
      border: 2px dashed rgba(217, 83, 30, 0.45);
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 14px; opacity: 0; pointer-events: none; transition: opacity 0.15s ease;
    }
    .editor-drop-overlay.active { opacity: 1; }
    .editor-drop-icon-wrap { position: relative; color: var(--accent-orange); }
    .editor-drop-plus {
      position: absolute; right: -8px; bottom: -8px; width: 22px; height: 22px;
      border-radius: 50%; background: var(--accent-orange); color: #FFFFFF;
      display: flex; align-items: center; justify-content: center;
      font-size: 1rem; font-weight: 800; line-height: 1;
      border: 2px solid var(--paper-white);
    }
    .editor-drop-text {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif;
      font-weight: bold; font-size: 1rem; text-transform: uppercase;
      color: var(--accent-orange); letter-spacing: 0.3px;
    }

    /* MLA FORMAT MODE */
    .editor-canvas-container.mla-mode .editor-canvas {
      font-family: 'Tinos', 'Times New Roman', Times, serif; font-size: 12pt; line-height: 2;
    }
    .editor-canvas-container.mla-mode .editor-canvas p,
    .editor-canvas-container.mla-mode .editor-canvas div {
      text-indent: 0.5in; margin-bottom: 0;
    }
    /* Real MLA formatting doesn't vary heading sizes — everything stays
       uniform 12pt double-spaced, same as body text — so headings typed via
       markdown shortcuts in MLA mode still work structurally (useful for a
       future document outline) but lose the visual size distinction. */
    .editor-canvas-container.mla-mode .editor-canvas h1,
    .editor-canvas-container.mla-mode .editor-canvas h2,
    .editor-canvas-container.mla-mode .editor-canvas h3 {
      font-size: 12pt; margin: 0; text-indent: 0.5in;
    }
    .doc-title-input.mla-mode {
      font-family: 'Tinos', 'Times New Roman', Times, serif; font-size: 12pt; font-weight: normal;
      text-align: center; padding-bottom: 0; margin-bottom: 0;
    }
    .mla-header-block {
      font-family: 'Tinos', 'Times New Roman', Times, serif; font-size: 12pt; line-height: 2;
      margin-bottom: 0;
    }
    .mla-header-block.hidden { display: none; }
    .mla-header-line {
      outline: none; white-space: pre-wrap; color: var(--text-dark);
    }
    .mla-header-line.placeholder-active { color: var(--text-muted); }
    .mla-header-block .doc-title-input { margin-top: 0; }

    /* STATUS & TARGET GOAL CONTAINER (BOTTOM RIGHT) */
    .status-widget-wrapper {
      position: fixed; bottom: 16px; right: 24px;
      z-index: 120; display: flex; flex-direction: column; align-items: flex-end;
      transition: opacity 0.4s ease;
      /* No gap here — the popover has its own margin-bottom, and the
         word-count-to-page-estimate spacing lives on .page-estimate-
         display's own margin-top instead (see its rule below), since
         THAT spacing specifically needs to animate to zero as part of
         its dissolve rather than being a flat, un-transitionable gap. */
    }
    .btn-mla-drawer {
      position: absolute; top: 75%; transform: translateY(-50%); left: -48px;
      width: 48px; height: 40px; border-radius: 8px 0 0 8px;
      background: var(--border-dark); color: var(--bg-cream);
      border: 2px solid var(--border-dark);
      font-family: 'Courier Prime', 'Courier New', monospace; font-weight: 700; font-size: 1.15rem;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      z-index: 110;
      transition: transform 0.15s, opacity 0.4s ease;
    }
    .btn-mla-drawer-label {
      letter-spacing: 0.07em;
      /* Trailing letter-spacing gap after the last glyph would otherwise
         throw off flex's centering (it centers this label's whole box,
         trailing gap included, not just the ink) — trimming that gap
         back out of the box keeps "MLA" optically centered in the tab. */
      margin-right: -0.07em;
    }
    .btn-mla-drawer:hover { transform: translateY(-50%) translateX(-4px); }

    .status-display-btn {
      background-color: transparent; border: none;
      padding: 4px 2px; font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1px;
      color: var(--text-muted); font-weight: 600; cursor: pointer;
      box-shadow: none; border-radius: 20px;
      transition: color 0.15s, background-color 0.2s, padding 0.2s, box-shadow 0.2s;
      display: flex; align-items: center; gap: 8px; user-select: none;
    }
    .status-display-btn:hover { color: var(--text-dark); }
    .status-display-btn.goal-active {
      background-color: var(--paper-white); border: 2px solid var(--border-dark);
      /* Asymmetric top/bottom (7px/5px, not the plain state's even 4px)
         — ITC Avant Garde's own metrics sit visually top-heavy within
         its line box, so a flat centered padding reads as the text
         sitting slightly above true-center. Shifts the flex-centered
         text down ~1px net without changing the pill's total height. */
      padding: 7px 14px 5px; color: var(--text-dark); font-weight: 700;
      box-shadow: 2px 2px 0px var(--border-dark);
    }
    .status-display-btn.goal-active:hover { transform: translateY(-1px); box-shadow: 3px 3px 0px var(--border-dark); }
    .status-display-btn.goal-completed {
      background-color: var(--paper-white); border: 2px solid var(--border-dark);
      padding: 7px 14px 5px; color: var(--accent-orange); font-weight: 700;
      animation: pulse-radar 1.4s infinite;
    }
    .page-estimate-display {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif;
      font-size: 0.62rem; text-transform: uppercase; letter-spacing: 1px;
      color: var(--text-muted); font-weight: 600; padding-right: 2px;
      line-height: 1.3;
      /* Collapses via max-height/opacity/margin (not a hard display:none)
         so hiding/revealing it as a goal starts/ends can actually
         transition — see .dissolving below and setPageEstimateVisible().
         20px comfortably covers one line at this font-size; .hidden
         still does the final display:none once the transition's done,
         so a fully-collapsed instance doesn't leave a dead interactive
         hit-area sitting behind the pill above it. */
      margin-top: 6px; max-height: 20px; opacity: 1; overflow: hidden;
      transition: opacity 0.3s ease, max-height 0.3s ease, margin-top 0.3s ease;
    }
    .page-estimate-display.dissolving { opacity: 0; max-height: 0; margin-top: 0; pointer-events: none; }
    .page-estimate-display.hidden { display: none; }

    @keyframes pulse-radar {
      0%   { box-shadow: 2px 2px 0px var(--border-dark), 0 0 0 0px rgba(217, 83, 30, 0.55); }
      70%  { box-shadow: 2px 2px 0px var(--border-dark), 0 0 0 10px rgba(217, 83, 30, 0); }
      100% { box-shadow: 2px 2px 0px var(--border-dark), 0 0 0 0px rgba(217, 83, 30, 0); }
    }

    .goal-menu-popover {
      background-color: var(--paper-white); border: 2px solid var(--border-dark);
      border-radius: 8px; padding: 8px 0; margin-bottom: 8px; width: 140px;
      box-shadow: 3px 3px 0px var(--border-dark); display: flex; flex-direction: column;
      align-items: stretch;
    }
    /* width:100% is what keeps every button's own box flush at one shared
       left edge — <button> sizes to its own text by default even inside
       a stretch-aligned flex column, so "Plain Text" and "PDF" were each
       hugging their own short content width while only "Word Document"
       (wide enough to wrap and hit the popover's actual width) happened
       to land flush left. justify-content:flex-start is what actually
       aligns the LABEL within that now-full-width box: the base `button`
       rule above makes every button `display:flex` with
       `justify-content:center` for icon+label buttons elsewhere in the
       app, which silently wins over text-align here (text-align doesn't
       apply to flex children) — the same override that comment already
       flags .workshop-add-item-btn as needing for the same reason. */
    .goal-menu-btn {
      background: transparent; border: none; padding: 6px 12px; text-align: left;
      width: 100%; box-sizing: border-box; justify-content: flex-start;
      font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; font-weight: 700;
      color: var(--text-dark); cursor: pointer; transition: background 0.12s;
    }
    .goal-menu-btn:hover { background-color: var(--bg-card); color: var(--accent-orange); }

    .export-menu {
      position: fixed; width: 190px; margin-bottom: 0; z-index: 300;
      overflow-y: auto;
    }
    /* Thin scrollbar, same spirit as the docnav tree only reflowing when
       window shrinkage actually forces it — this only ever shows up
       when openExportMenu() below has detected the menu doesn't fit
       under the button and capped its height, so it's invisible in the
       normal/full-screen case. */
    .export-menu::-webkit-scrollbar { width: 6px; }
    .export-menu::-webkit-scrollbar-track { background: transparent; }
    .export-menu::-webkit-scrollbar-thumb { background: var(--text-muted); border-radius: 3px; }

    /* THESAURUS SIDE PANEL */
    .thesaurus-panel {
      position: fixed; top: 0; right: -360px; width: 340px; height: 100vh;
      background-color: var(--paper-white); border-left: 2px solid var(--border-dark);
      z-index: 150; display: flex; flex-direction: column; padding: 30px 24px;
      box-shadow: -4px 0px 12px rgba(0, 0, 0, 0.05); transition: right 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    }
    .thesaurus-panel.active { right: 0; }
    .thesaurus-header {
      display: flex; justify-content: space-between; align-items: flex-start;
      padding-bottom: 12px; border-bottom: 2px solid var(--border-dark); margin-bottom: 16px;
    }
    .thesaurus-word-title { font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1.8rem; font-weight: bold; color: var(--text-dark); word-break: break-word; }
    .thesaurus-pos { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--thesaurus-blue); font-weight: 700; margin-top: 4px; }
    .thesaurus-close-btn { background: transparent; border: none; font-size: 1.2rem; color: var(--text-muted); cursor: pointer; padding: 0 4px; line-height: 1; }
    .thesaurus-close-btn:hover { color: var(--border-dark); }
    
    .thesaurus-definition {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; font-style: italic; color: var(--text-dark);
      background-color: var(--bg-cream); border-left: 3px solid var(--thesaurus-blue);
      padding: 8px 10px; margin-bottom: 16px; line-height: 1.4; border-radius: 0 4px 4px 0;
    }

    .thesaurus-content { flex-grow: 1; overflow-y: auto; padding-right: 6px; }
    .synonym-section { margin-bottom: 24px; }
    .synonym-section-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-muted); font-weight: 700; margin-bottom: 12px; padding-bottom: 4px; border-bottom: 1px dashed #DDD8CE; }
    .synonym-list { display: flex; flex-wrap: wrap; gap: 8px; }
    .synonym-chip { background-color: var(--bg-cream); border: 1.5px solid var(--border-dark); padding: 6px 12px; border-radius: 4px; font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.9rem; color: var(--text-dark); cursor: pointer; transition: all 0.15s ease; }
    /* Was four separate hover overrides (base/thesaurus, grammar,
       spell, rhyme), each hardcoding its own panel's accent color
       individually — consolidated into one rule reading
       --panel-accent/-dark, which every panel (.thesaurus-panel's
       variants below, and .write-panel above) already defines for
       exactly this purpose. One rule that's automatically correct
       everywhere instead of four that each need to be remembered and
       kept in sync by hand. */
    .synonym-chip:hover { background-color: var(--panel-accent); color: #FFFFFF; border-color: var(--panel-accent-dark); transform: translateY(-2px); box-shadow: 2px 2px 0px var(--border-dark); }
    .synonym-chip.strong { font-weight: bold; }

    .thesaurus-panel { --panel-accent: var(--thesaurus-blue); --panel-accent-dark: var(--thesaurus-blue-dark); }
    .thesaurus-panel.spell-variant { --panel-accent: var(--spell-teal); --panel-accent-dark: var(--spell-teal-dark); }
    .thesaurus-panel.spell-variant .thesaurus-pos { color: var(--spell-teal); }
    .thesaurus-panel.spell-variant .thesaurus-definition { border-left-color: var(--spell-teal); }
    .thesaurus-panel.rhyme-variant { --panel-accent: var(--rhyme-purple); --panel-accent-dark: var(--rhyme-purple-dark); }
    .thesaurus-panel.rhyme-variant .thesaurus-pos { color: var(--rhyme-purple); }

    .panel-search-row { display: flex; gap: 8px; margin-bottom: 16px; }
    .panel-search-input-wrap { position: relative; flex-grow: 1; display: flex; }
    .panel-search-input {
      flex-grow: 1; padding: 10px 30px 6px 16px; border: 2px solid var(--border-dark);
      border-radius: 4px; font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem;
      background: var(--bg-cream); outline: none; color: var(--text-dark);
      /* Back to padding, not transform — the complaint was specifically
         about the TEXT sitting high relative to its OWN caret, both
         living inside this same box. A transform on the whole input
         moves text and caret together as a unit (no effect on their
         relative position) — it only ever helped the box's position
         relative to the search button beside it, a separate concern.
         Padding is the thing that actually shifts text+caret within
         their own box: more top, less bottom (16 total, same as
         before, so the box's own height is unchanged), plus more room
         on the left so the caret/text don't start flush against the
         border. */
    }
    .panel-search-input:focus { border-color: var(--panel-accent); }
    /* "Did You Mean?" specifically — its placeholder ("Have you ever had
       a dream, that...") is the longest of the three panel search
       inputs and was overflowing/getting cut off at the shared base
       size. Scoped to this one input by id rather than lowering the
       shared .panel-search-input size for Thesaurus/Rhymes too. */
    #spell-search-input { font-size: 0.72rem; }
    .panel-clear-btn {
      position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
      background: transparent; border: none; color: var(--text-muted);
      font-size: 0.8rem; cursor: pointer; padding: 4px; line-height: 1;
      display: flex; align-items: center; justify-content: center;
    }
    .panel-clear-btn:hover { color: var(--border-dark); }
    .panel-search-btn {
      background-color: var(--panel-accent); border: 2px solid var(--border-dark);
      border-radius: 4px; width: 38px; flex-shrink: 0; cursor: pointer;
      font-size: 0.95rem; box-shadow: 2px 2px 0px var(--border-dark);
      transition: background-color 0.15s, transform 0.15s;
    }
    .panel-search-btn:hover { background-color: var(--panel-accent-dark); transform: translateY(-1px); }

    /* ARCADE INVENTORY-SLOT TREATMENT — Thesaurus/Did You Mean?/Grammar
       panel interiors, Build-an-Essay only. These panels are shared
       singleton elements (same DOM whether opened from the plain editor
       or the workshop — see body.workshop-mode's own top comment), so
       body.workshop-mode is what scopes this to the workshop specifically
       without touching how they look in the Document Center. */
    body.workshop-mode .synonym-chip,
    body.workshop-mode .thesaurus-definition,
    body.workshop-mode .panel-search-input,
    body.workshop-mode .panel-search-btn {
      border-radius: 0;
    }
    /* Chunky border + a genuine inset shadow (not just the existing
       hard OUTSET shadow every arcade element already gets) — the
       inset reads as a slight recess punched into the slot, like a
       physical cartridge/chip socket, layered inside the same border. */
    body.workshop-mode .synonym-chip {
      border-width: 2px;
      box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.15), inset -1px -1px 0px 0px rgba(255, 255, 255, 0.08);
      transition: transform 0.08s ease, box-shadow 0.08s ease, background-color 0.12s ease, border-color 0.12s ease;
    }
    /* Hover: border itself becomes the neon accent (on top of the
       existing fill/lift), reinforcing "selected" the way an arcade
       cursor outline would, not just a background swap. */
    body.workshop-mode .synonym-chip:hover {
      border-color: var(--panel-accent-dark);
      box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.25), 2px 2px 0px var(--shadow-hard);
    }
    /* Press: the classic hard-shadow "pushed in" read (see .doc-card,
       .reasoning-ref-btn, and every other pressable arcade element this
       session) — displaced 2px down-right with the shadow flattened to
       match, instead of the soft translateY(-2px)/scale hover-lift this
       chip still uses everywhere outside the workshop. */
    body.workshop-mode .synonym-chip:active {
      transform: translate(2px, 2px);
      box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.35);
    }
    body.workshop-mode .thesaurus-definition { border-left-width: 4px; }
    body.workshop-mode .panel-search-input {
      box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.15);
    }
    /* Pixel-art replacements for the search button's 🔍 emoji and the
       clear button's plain "✕" — neither was ever a smooth vector icon
       to begin with (emoji/plain text glyph), but both still read as
       soft/modern next to the rest of this treatment. Hand-built as
       crisp rect-grid SVGs (same shape-rendering:crispEdges convention
       as the app's other pixel icons, e.g. the castle) rather than an
       external asset, then image-rendering:pixelated so scaling never
       softens them. The search icon is a flat white background-image
       (its button is always a solid accent fill, so a fixed white
       glyph reads fine in both rest/hover states); the clear icon uses
       mask-image instead so it's colored by currentColor and keeps
       reacting to its own :hover color change like the text glyph did. */
    body.workshop-mode .panel-search-btn {
      font-size: 0;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' shape-rendering='crispEdges'%3E%3Crect x='3' y='2' width='4' height='1' fill='white'/%3E%3Crect x='2' y='3' width='1' height='4' fill='white'/%3E%3Crect x='7' y='3' width='1' height='4' fill='white'/%3E%3Crect x='3' y='7' width='4' height='1' fill='white'/%3E%3Crect x='8' y='8' width='1' height='1' fill='white'/%3E%3Crect x='9' y='9' width='1' height='1' fill='white'/%3E%3Crect x='10' y='10' width='2' height='2' fill='white'/%3E%3C/svg%3E");
      background-repeat: no-repeat; background-position: calc(50% - 2px) calc(50% - 2px); background-size: 21px 21px;
      image-rendering: pixelated;
    }
    body.workshop-mode .panel-clear-btn {
      font-size: 0; width: 14px; height: 14px;
      background-color: currentColor;
      -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='2' height='2'/%3E%3Crect x='6' y='0' width='2' height='2'/%3E%3Crect x='2' y='2' width='2' height='2'/%3E%3Crect x='4' y='2' width='2' height='2'/%3E%3Crect x='2' y='4' width='2' height='2'/%3E%3Crect x='4' y='4' width='2' height='2'/%3E%3Crect x='0' y='6' width='2' height='2'/%3E%3Crect x='6' y='6' width='2' height='2'/%3E%3C/svg%3E");
      mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' shape-rendering='crispEdges'%3E%3Crect x='0' y='0' width='2' height='2'/%3E%3Crect x='6' y='0' width='2' height='2'/%3E%3Crect x='2' y='2' width='2' height='2'/%3E%3Crect x='4' y='2' width='2' height='2'/%3E%3Crect x='2' y='4' width='2' height='2'/%3E%3Crect x='4' y='4' width='2' height='2'/%3E%3Crect x='0' y='6' width='2' height='2'/%3E%3Crect x='6' y='6' width='2' height='2'/%3E%3C/svg%3E");
      -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
      -webkit-mask-position: center; mask-position: center;
      -webkit-mask-size: 10px 10px; mask-size: 10px 10px;
      image-rendering: pixelated;
    }
    .thesaurus-empty-state { font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.95rem; color: var(--text-muted); line-height: 1.6; font-style: italic; margin-top: 20px; }

    /* GRAMMAR & WORKSHOP PANEL */
    .write-panel {
      position: fixed; top: 0; right: -520px; width: 480px; height: 100vh;
      background-color: var(--paper-white); border-left: 2px solid var(--border-dark);
      z-index: 150; display: flex; flex-direction: column; padding: 30px 24px;
      box-shadow: -4px 0px 12px rgba(0, 0, 0, 0.05); transition: right 0.3s cubic-bezier(0.16, 1, 0.3, 1);
      /* --panel-accent/-dark, same pattern .thesaurus-panel's variants
         use — .synonym-chip (shared with Grammar's suggestion chips)
         reads these directly, so defining them here instead of leaving
         Grammar's chips on a separate hardcoded override keeps every
         panel's chip hover correctly tied to its own real accent
         through one shared rule, not several. */
      --panel-accent: var(--write-burgundy); --panel-accent-dark: var(--write-burgundy-dark);
    }
    .write-panel.active { right: 0; }
    /* Build-an-Essay's Grammar bubble hides Phrasing entirely (see
       handleWorkshopGrammarTrigger) — with only one tab's worth of
       content, the panel should match the width of the other three
       (Thesaurus/Did You Mean/Rhymes, all 340px) instead of staying at
       the full 480px sized for two tabs side by side. */
    .write-panel.grammar-only {
      width: 340px; right: -360px;
    }
    .write-panel.grammar-only.active { right: 0; }
    .write-panel-body { flex-grow: 1; overflow-y: auto; }
    #mla-tab-citations-content, #mla-tab-workscited-content, #mla-tab-formatting-content { padding-top: 16px; }

    .write-tabs { display: flex; gap: 10px; margin: 18px 0; }
    .write-tab-btn {
      flex: 1; padding: 11px 8px; border: 2px solid var(--border-dark); border-radius: 6px;
      background: var(--bg-cream); font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: 800; font-size: 0.78rem;
      text-transform: uppercase; letter-spacing: 1px; cursor: pointer; text-align: center;
      transition: background-color 0.15s, color 0.15s; color: var(--text-dark);
    }
    /* Scoped to just these two (not the shared .write-tab-btn base, and
       not .mla-tab, which stays at its original size) — ITC Avant Garde
       Gothic Demi's tall cap-height was only called out as visually high
       on GRAMMAR/PHRASING specifically. Bumped up from the base 0.78rem
       into the 14-15px range and given a matching, slightly looser
       tracking so the larger uppercase text doesn't read as cramped. */
    .write-tab-btn.grammar-tab, .write-tab-btn.phrasing-tab {
      display: inline-flex; align-items: center; justify-content: center;
      font-size: 0.9rem; letter-spacing: 0.06em;
    }
    /* text-align:center (inherited from the base rule) only centers
       inline content horizontally — switching to inline-flex above
       means the inner span needs its own align-items:center centering
       too, and the translateY nudge below needs a positioned/flex text
       node to shift independently of the button's own box, rather than
       nudging the whole button (which would eat into its padding). */
    .write-tab-label { transform: translateY(2px); }
    .write-tab-btn.grammar-tab.active { background: var(--border-dark); color: var(--paper-white); }
    .write-tab-btn.phrasing-tab.active { background: var(--write-burgundy); color: #FFFFFF; border-color: var(--write-burgundy); }
    /* Same idea as .grammar-tab/.phrasing-tab above (inline-flex
       centering + a dedicated label to nudge vertically), but NOT the
       same values — measured separately rather than copied, since
       these three tabs split the drawer's full 480px width three ways
       (~137px each) instead of Grammar/Phrasing's two-way split
       (~211px each) in that same panel. Checked directly: "WORKS
       CITED"/"FORMATTING" (the longest labels here) still clear their
       ~113px content box comfortably at 0.82rem (17px+ to spare) —
       0.9rem like Grammar/Phrasing would leave under 10px and starts
       crowding the border on a narrower run of the font. */
    .write-tab-btn.mla-tab {
      display: inline-flex; align-items: center; justify-content: center;
      font-size: 0.82rem;
    }
    .mla-tab-label { transform: translateY(1.5px); }
    .write-tab-btn.mla-tab.active { background: var(--border-dark); color: var(--paper-white); }
    /* Same hardcoded-not-variable pattern as every other tab's own active
       color above (.grammar-tab.active, .phrasing-tab.active) — comes
       after .mla-tab.active in source order (equal specificity, both are
       three-class selectors) so it wins on the two tabs that carry both
       classes. Hardcoded rather than swapped per light/dark theme, same
       reasoning as the yellow bubble-selection frame and the read-aloud
       mascot elsewhere: an arcade accent with its own fixed identity. */
    .write-tab-btn.essay-tab.active { background: #7B2CBF; color: #FFFFFF; border-color: #7B2CBF; }
    /* Build-an-Essay's grammar-only panel has nothing to switch between
       (Phrasing is hidden entirely there) — a single permanently-active
       tab was just dead weight, and hiding it brings this panel in line
       with the other three (Thesaurus/Did You Mean/Rhymes), none of
       which show a tab bar either. */
    .write-panel.grammar-only .write-tabs { display: none; }

    #grammar-tab-content ::selection { background-color: var(--write-burgundy); color: #FFFFFF; }

    .grammar-issue {
      border: 2px solid var(--border-dark); border-radius: 8px; padding: 14px 14px 32px;
      margin-bottom: 14px; background: var(--bg-cream); position: relative;
    }
    .grammar-issue.no-goto { padding-bottom: 14px; }
    /* Quest-log card: hard corners, a colored stripe down the left edge
       instead of a rounded soft card. Real LanguageTool categories
       (data-category, set in JS where these cards are built) drive the
       color where a meaningful distinction actually exists — TYPOS in
       red, PUNCTUATION/CASING in gold, matching the ticket's own named
       examples — with GRAMMAR/STYLE/everything else defaulting to the
       panel's own write-burgundy rather than a fabricated category
       scheme for data the API doesn't actually distinguish that finely. */
    body.workshop-mode .grammar-issue {
      border-radius: 0; border-left-width: 5px; border-left-color: var(--write-burgundy);
      box-shadow: 3px 3px 0px var(--shadow-hard);
    }
    body.workshop-mode .grammar-issue[data-category="TYPOS"] { border-left-color: var(--write-burgundy-dark); }
    body.workshop-mode .grammar-issue[data-category="PUNCTUATION"],
    body.workshop-mode .grammar-issue[data-category="CASING"] { border-left-color: var(--speak-mustard-dark); }
    .grammar-ignore-btn {
      position: absolute; top: 10px; right: 12px;
      background: none; border: none; color: var(--text-muted);
      font-size: 0.65rem; font-weight: 800; text-transform: uppercase;
      letter-spacing: 0.5px; cursor: pointer; padding: 2px 4px;
    }
    .grammar-ignore-btn:hover { color: var(--border-dark); text-decoration: underline; }
    /* Retro terminal command style — monospace, bracketed like a CLI
       flag, no underline (a real terminal command doesn't get one;
       the color shift on hover is the whole affordance). */
    body.workshop-mode .grammar-ignore-btn {
      font-family: 'Courier Prime', 'Courier New', monospace; font-weight: 700;
      font-size: 0.73rem;
    }
    body.workshop-mode .grammar-ignore-btn::before { content: '[ '; }
    body.workshop-mode .grammar-ignore-btn::after { content: ' ]'; }
    body.workshop-mode .grammar-ignore-btn:hover { text-decoration: none; color: var(--write-burgundy-dark); }
    .grammar-goto-btn {
      position: absolute; bottom: 10px; right: 12px;
      background: none; border: none; color: var(--accent-orange);
      font-size: 0.68rem; font-weight: 800; text-transform: uppercase;
      letter-spacing: 0.5px; cursor: pointer; padding: 2px 4px;
    }
    .grammar-goto-btn:hover { text-decoration: underline; }
    /* Scoped under .editor-canvas specifically (not just the bare class)
       so its specificity (2 classes) beats the Clean Paste sanitizer rule
       above (.editor-canvas *:not(...), which resolves to 1 class + 4
       type selectors via :not()) — confirmed directly that without this,
       the sanitizer's background-color:transparent !important silently
       wins regardless of source order, since both rules carry !important
       and cascade among those is still specificity-ordered. That's why
       the flash never visibly appeared at all, not just briefly. */
    .editor-canvas .grammar-locate-flash {
      background-color: rgba(217, 83, 30, 0.35) !important;
      border-radius: 2px;
      transition: background-color 1s ease-out;
    }
    .editor-canvas .grammar-locate-flash.fade { background-color: transparent !important; }
    .grammar-issue-msg {
      font-weight: 700; font-size: 0.8rem; margin-bottom: 8px; color: var(--text-dark);
      /* padding-right reserves room for the absolutely-positioned IGNORE
         button (~54px including its own padding/letter-spacing) so a
         long message wraps around it instead of running underneath it —
         confirmed happening especially in Build-an-Essay's narrower
         340px panel, where lines wrap sooner and reach that corner more
         often, but this applies equally to the editor's wider panel too
         since it's the exact same shared markup and class either way. */
      padding-right: 56px;
    }
    .grammar-issue-snippet {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-style: italic; font-size: 0.85rem;
      color: var(--text-muted); margin-bottom: 10px; line-height: 1.4;
    }

    .phrasing-key-setup { text-align: left; }
    .phrasing-key-setup .panel-search-row { margin: 14px 0 8px; }
    .phrasing-key-note { font-size: 0.72rem; color: var(--text-muted); line-height: 1.5; margin-top: 6px; }
    .phrasing-key-error { font-size: 0.75rem; font-weight: 700; color: #D42E2E; margin: 6px 0 10px; }
    .mla-ref-block { margin-bottom: 34px; }
    .mla-ref-heading {
      font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.78rem; font-weight: 800;
      text-transform: uppercase; letter-spacing: 0.5px; color: var(--border-dark); margin-bottom: 4px;
      text-decoration: underline; text-underline-offset: 2px;
    }
    .mla-ref-body { font-size: 0.85rem; line-height: 1.55; color: var(--text-dark); }
    .mla-ref-body em { font-style: italic; }
    .mla-ref-example {
      display: block; width: fit-content; margin: 6px 0; padding: 7px 10px;
      background: var(--bg-cream); border: 1px solid rgba(34, 34, 34, 0.15); border-radius: 4px;
      font-family: 'Courier New', monospace; font-weight: 400; font-size: 0.92rem; color: var(--border-dark);
      max-width: 100%;
    }
    .mla-ref-footer {
      font-size: 0.72rem; color: var(--text-muted); line-height: 1.6;
      border-top: 1px solid rgba(34, 34, 34, 0.18); padding-top: 14px; margin-top: 4px;
    }
    .mla-ref-footer a { color: var(--border-dark); font-weight: 700; }
    /* TRANSITIONS TAB (Stage 5's purple side panel) */
    .transitions-intro {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; line-height: 1.55;
      color: var(--text-dark); margin-bottom: 24px;
    }
    /* Bolded + highlighted per Ethan — the same "category label ahead of
       its own block" role .mla-ref-heading plays on the Citations tab,
       just a filled chip instead of an underline so Words/Phrases read
       as clearly distinct at a glance. */
    .transitions-section-heading {
      display: inline-block; font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-weight: 700;
      font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.5px; color: #FFFFFF;
      background: #7B2CBF; padding: 3px 10px; border-radius: 4px; margin-bottom: 10px;
    }
    /* Avant Garde's own glyphs sit noticeably top-heavy within their line
       box (same optical issue as .mla-tab-label/.evidence-type-btn-label
       elsewhere) — a couple px nudge down centers the actual ink inside
       the chip's padding instead of reading high. */
    .transitions-section-heading-text { display: inline-block; transform: translateY(1.5px); }
    .transitions-chip-list { display: flex; flex-wrap: wrap; gap: 8px; }
    .transitions-chip {
      display: inline-block; padding: 6px 10px; background: var(--bg-cream);
      border: 1px solid rgba(123, 44, 191, 0.35); border-radius: 4px;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.82rem; color: var(--text-dark);
    }
    .phrasing-change-key-link {
      display: inline-block; font-family: 'ITC Avant Garde Gothic Book', sans-serif;
      font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px;
      color: var(--text-muted); cursor: pointer; margin-bottom: 14px; text-decoration: underline;
    }
    .phrasing-change-key-link:hover { color: var(--write-burgundy); }
    .phrasing-suggestion {
      border: 2px solid var(--border-dark); border-radius: 8px; padding: 14px;
      margin-bottom: 14px; background: var(--bg-cream);
    }
    .phrasing-suggestion-text {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.95rem; line-height: 1.55;
      color: var(--text-dark); margin-bottom: 10px;
    }
    .phrasing-suggestion-footer { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
    .phrasing-suggestion-note { font-size: 0.72rem; color: var(--text-muted); font-style: italic; flex-grow: 1; }
    .btn-action.primary.phrasing-use-btn { background-color: var(--write-burgundy); border-color: var(--write-burgundy); }
    /* Start Word Sprint / Start Timer button — same "border matches fill"
       treatment as the Phrasing button above, so the orange runs flush
       to the edges instead of showing a black outline. */
    .btn-action.primary.goal-sprint-btn { border-color: var(--accent-orange); }
    .phrasing-use-btn { flex-shrink: 0; padding: 7px 12px; font-size: 0.68rem; }
    .phrasing-use-btn:hover { background-color: var(--write-burgundy-dark); border-color: var(--write-burgundy-dark); }

    /* SETTINGS PANEL & MODALS */
    .settings-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.15); z-index: 200; opacity: 0; pointer-events: none; transition: 0.2s; }
    .settings-overlay.active { opacity: 1; pointer-events: auto; }
    .settings-panel {
      position: fixed; top: 0; right: -420px; width: 400px; height: 100vh;
      background-color: var(--bg-cream); border-left: 2px solid var(--border-dark);
      z-index: 201; padding: 30px 32px;
      transition: right 0.32s cubic-bezier(0.16, 1, 0.3, 1);
      display: flex; flex-direction: column; overflow-y: auto;
      box-shadow: -4px 0px 12px rgba(0, 0, 0, 0.05);
    }
    .settings-panel::-webkit-scrollbar { width: 10px; }
    .settings-panel::-webkit-scrollbar-track { background: transparent; }
    .settings-panel::-webkit-scrollbar-thumb { background-color: var(--text-muted); border-radius: 10px; border: 2px solid var(--bg-cream); }
    .settings-panel.active { right: 0; }
    .settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 10px; border-bottom: 2px solid var(--border-dark); }
    .settings-header-actions { display: flex; align-items: center; gap: 18px; }
    .settings-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; font-family: 'ITC Avant Garde Gothic Demi', sans-serif; }
    .setting-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 2px 0; }
    /* Tight line-height so the label's own text box hugs its actual
       glyph height instead of the app-wide 1.5 default — at 1.5, the
       label's line-box is noticeably taller than the 22px toggle switch
       next to it, so flex's align-items:center was centering against
       that inflated box rather than the visible text, leaving the label
       looking off-baseline against the toggle icon. */
    .setting-label { min-width: 190px; font-size: 0.8rem; font-weight: 600; line-height: 1;
      font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      /* Avant Garde Demi's cap-height still sits slightly above center
         even at line-height:1 — this drops it level with the toggle
         switch's true geometric center next to it. transform instead of
         padding-top: as a flex item, padding would add to this label's
         own box size and feed back into align-items:center's own
         centering math on .setting-item, working against the very
         correction it's trying to make. transform is purely visual. */
      display: inline-block; transform: translateY(1.5px); }
    .comfort-zoom-steps { display: flex; gap: 6px; margin-top: 2px; }
    .comfort-zoom-step-btn {
      background: var(--paper-white); border: 2px solid var(--border-dark); color: var(--text-dark);
      font-size: 0.7rem; font-weight: 700; padding: 4px 10px; border-radius: 20px;
      cursor: pointer; transition: background-color 0.15s, color 0.15s;
    }
    .comfort-zoom-step-btn:hover { background-color: var(--bg-card); }
    .comfort-zoom-step-btn.active { background-color: var(--border-dark); color: var(--paper-white); }
    .btn-manual {
      width: 100%; margin-top: 10px; padding: 12px;
      background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 6px;
      font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: 800; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1.5px;
      cursor: pointer; color: var(--text-dark);
      box-shadow: 2px 2px 0px var(--border-dark);
      transition: background-color 0.15s, transform 0.15s;
      /* Space between "THE MANUAL" and the emoji — the button is already
         display:inline-flex/align-items:center from the global button
         rule, so gap here is enough on its own without touching either
         child's own margin. */
      gap: 6px;
    }
    .btn-manual:hover { background: var(--bg-card); transform: translateY(-1px); }
    /* Text-only nudge — the emoji is already correctly centered on its
       own glyph metrics, so only "THE MANUAL" (its own span) needs the
       optical correction, not the emoji or the button's own bounds. */
    .btn-manual-label { display: inline-block; transform: translateY(1.5px); }
    .btn-manual-emoji { display: inline-flex; align-items: center; align-self: center; }

    /* HOW-TO GUIDE */
    .howto-overlay {
      position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 400;
      opacity: 0; pointer-events: none; transition: opacity 0.2s;
    }
    .howto-overlay.active { opacity: 1; pointer-events: auto; }
    .howto-modal {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.96);
      width: 640px; max-width: 90vw; max-height: 82vh;
      background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 10px;
      box-shadow: 6px 6px 0px var(--border-dark); z-index: 401;
      opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s;
      display: flex; flex-direction: column; overflow: hidden;
      /* One shared line-height for everything in the modal, inherited
         down to the title/subtitle/section titles/badge glyphs — the
         one exception is <p> (see .howto-section-body p below), which
         needs its own explicit copy of this same value since a global
         `p, span, h1-h6, label { line-height: 1.2; }` reset elsewhere
         matches <p> directly and always beats an inherited value. */
      line-height: 1.65;
    }
    .howto-modal.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
    .howto-header {
      display: flex; justify-content: space-between; align-items: flex-start;
      padding: 22px 28px 16px; border-bottom: 3px double var(--border-dark); flex-shrink: 0;
    }
    .howto-title { font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1.6rem; font-weight: bold; color: var(--text-dark); }
    .howto-subtitle { font-family: 'Courier New', monospace; font-size: 0.7rem; letter-spacing: 2px; text-transform: uppercase; color: var(--text-muted); margin-top: 4px; }
    .howto-close-btn { background: transparent; border: none; font-size: 1.3rem; color: var(--text-muted); cursor: pointer; padding: 0 4px; line-height: 1; }
    .howto-close-btn:hover { color: var(--border-dark); }

    /* overscroll-behavior:contain stops scroll chaining at this
       container's own top/bottom edge — without it, once you scroll
       past the very top or bottom, the browser hands the rest of that
       scroll gesture to the next scrollable ancestor, which is the
       page behind the modal (position:fixed doesn't exempt it from
       scroll chaining; only this property does). */
    .howto-body { padding: 26px 28px 34px; overflow-y: auto; flex-grow: 1; overscroll-behavior: contain; }
    /* Genuine padding on the section itself, not just leaning on
       .howto-divider's own 26px top/bottom margin to carry all the
       separation — that margin already keeps headers well clear of the
       dashed line above them, but this makes that breathing room an
       explicit property of the card, not an incidental side effect of
       its neighbor's spacing. */
    .howto-section { padding-top: 12px; }
    .howto-section-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
    /* position:relative + a negative top nudges every badge circle up
       3px without disturbing the flex layout around it (top/left offsets
       from `position:relative` don't affect the space the element itself
       reserves, so the title text next to it in .howto-section-header —
       or the row height in .howto-icon-row — doesn't shift too). Shared
       by every badge use in the modal, in headers and in the icon row
       alike, since both reuse this one class. */
    .howto-badge {
      position: relative; top: -3px;
      width: 34px; height: 34px; border-radius: 50%; border: 2px solid var(--border-dark);
      display: flex; align-items: center; justify-content: center; flex-shrink: 0;
      font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: bold; font-size: 1.05rem; color: #FFFFFF;
      box-shadow: 2px 2px 0px var(--border-dark);
    }
    .howto-badge.badge-orange { background: var(--accent-orange); }
    .howto-badge.badge-blue { background: var(--thesaurus-blue); }
    .howto-badge.badge-teal { background: var(--spell-teal); }
    .howto-badge.badge-burgundy { background: var(--write-burgundy); }
    .howto-badge.badge-dark { background: var(--border-dark); color: var(--paper-white); }
    .howto-badge.badge-rhyme { background: var(--rhyme-purple); }
    .header-rhyme .howto-section-title { color: var(--rhyme-purple); }
    .howto-badge.badge-speak { background: var(--speak-mustard); }
    .header-speak .howto-section-title { color: var(--speak-mustard); }
    .howto-section-title { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 1.05rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dark); }
    .header-orange .howto-section-title { color: var(--accent-orange); }
    .header-blue .howto-section-title { color: var(--thesaurus-blue); }
    .header-teal .howto-section-title { color: var(--spell-teal); }
    .header-burgundy .howto-section-title { color: var(--write-burgundy); }
    /* This container's own line-height:1.65 looked like it already
       covered the paragraphs below, but a global reset elsewhere
       (`p, span, h1-h6, label { line-height: 1.2; }`) directly targets
       <p> itself — an inherited value from an ancestor always loses to
       ANY rule that matches the element directly, regardless of which
       rule is more specific, so every paragraph in here was actually
       rendering at 1.2, not 1.65. Setting it again here, directly on
       `.howto-section-body p`, is what actually wins against that reset. */
    .howto-section-body { font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.92rem; line-height: 1.65; color: var(--text-dark); }
    .howto-section-body p { margin: 0 0 16px 0; line-height: 1.65; }
    .howto-section-body p:last-child { margin-bottom: 0; }
    /* Word Search Tools' three badges sit in their own row below the
       header instead of one badge inside it — same .howto-badge circles
       reused as-is, just laid out side by side since there's no single
       icon that represents Thesaurus + "Did You Mean?" + Rhymes at once. */
    .howto-icon-row { display: flex; gap: 12px; margin-bottom: 16px; }
    /* Modifier for the "What's Next?" section's title only — every other
       title inherits .howto-section-title's own bold+uppercase as-is,
       this just adds the italic on top for that one heading. */
    .howto-title-italic { font-style: italic; }
    .howto-next-list { margin: 0; padding-left: 22px; }
    .howto-next-list li { margin-bottom: 8px; }
    .howto-next-list li:last-child { margin-bottom: 0; }
    .howto-divider { border: none; border-top: 1px dashed #C9C3B6; margin: 26px 0; }

    .switch { position: relative; display: inline-block; width: 40px; height: 22px; }
    .switch input { opacity: 0; width: 0; height: 0; }
    .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--bg-card); border: 2px solid var(--border-dark); transition: .2s; border-radius: 22px; }
    .slider:before { position: absolute; content: ""; height: 12px; width: 12px; left: 3px; bottom: 3px; background-color: var(--border-dark); transition: .2s; border-radius: 50%; }
    input:checked + .slider { background-color: var(--accent-orange); }
    input:checked + .slider:before { transform: translateX(18px); background-color: var(--paper-white); }

    .dark-mode-toggle {
      width: 38px; height: 38px; border-radius: 50%; border: 2px solid var(--border-dark);
      background: var(--bg-card); display: flex; align-items: center; justify-content: center;
      cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      transition: background-color 0.25s, transform 0.3s, box-shadow 0.2s;
    }
    .dark-mode-toggle svg { width: 17px; height: 17px; fill: var(--text-muted); transition: fill 0.25s; }
    .dark-mode-toggle:hover { transform: translateY(-1px); }
    .dark-mode-toggle.active { background: #241b33; transform: rotate(-18deg); }
    .dark-mode-toggle.active svg { fill: #b48ee8; }

    .sound-mute-toggle {
      width: 38px; height: 38px; border-radius: 50%; border: 2px solid var(--border-dark);
      background: var(--bg-card); display: flex; align-items: center; justify-content: center;
      cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      transition: background-color 0.25s, transform 0.2s, box-shadow 0.2s;
    }
    .sound-mute-toggle svg { width: 17px; height: 17px; fill: var(--text-muted); transition: fill 0.25s; overflow: visible; }
    .sound-mute-toggle:hover { transform: translateY(-1px); }
    /* The strike-through line is a real SVG element, not a CSS trick —
       hidden by default, shown only when muted, along with dimming (not
       fully hiding) the sound-wave arcs so it reads as "crossed out"
       rather than a completely different icon. */
    .sound-mute-toggle .sound-mute-line {
      display: none; stroke: var(--accent-orange); stroke-width: 2; stroke-linecap: round;
    }
    .sound-mute-toggle.active { background: var(--bg-card); }
    .sound-mute-toggle.active svg { fill: var(--accent-orange); }
    .sound-mute-toggle.active .sound-mute-line { display: block; }
    .sound-mute-toggle.active .sound-wave-1,
    .sound-mute-toggle.active .sound-wave-2 { opacity: 0.25; }

    .btn-setting-action {
      background-color: var(--paper-white); border: 2px solid var(--border-dark);
      border-radius: 4px; padding: 6px 12px; font-family: inherit; font-size: 0.72rem;
      font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dark);
      cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark); transition: all 0.12s ease;
    }
    /* #FFFFFF, not var(--paper-white) — same fix/reasoning as
       .exit-bubble:hover above. */
    .btn-setting-action:hover { background-color: var(--accent-orange); color: #FFFFFF; transform: translateY(-1px); }

    /* TARGET GOAL SETUP DROPDOWN */
    .goal-dropdown {
      position: absolute; top: calc(100% + 10px); right: 0;
      width: 280px; background-color: var(--paper-white); border: 2px solid var(--border-dark);
      border-radius: 8px; box-shadow: 4px 4px 0px var(--border-dark); padding: 20px; z-index: 116;
      opacity: 0; pointer-events: none; transform-origin: top right; transform: translateY(-6px) scale(0.97);
      transition: opacity 0.16s ease, transform 0.16s ease;
    }
    .goal-dropdown.active { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); }
    /* will-change scoped to :hover on the trigger button, not applied
       permanently — a click is almost always preceded by at least a
       few ms of hovering the button first, which is enough for Chrome
       to promote this to its own compositor layer before the actual
       open, without paying the (real, if modest) memory/layer-
       management cost of leaving every popup like this permanently
       promoted whether it's ever opened or not. Removed automatically
       the instant the hover ends, no JS bookkeeping needed. */
    #goal-action-btn:hover + .goal-dropdown { will-change: transform, opacity; }
    /* translateY(2.5px) nudges here (and on the tab/input-label/helper-
       text/button rules below) are a shared optical correction — this
       modal's Demi/Book faces sit high within their line box the same
       way ITC Avant Garde Gothic does elsewhere in the app (see
       .btn-spell-bubble-glyph's own translateY nudge), noticeable
       enough across this many stacked text elements to be worth the
       nudge everywhere at once. Pure transform (no layout impact) here
       since this element has no border of its own to visually
       displace. */
    .goal-modal-title { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 2px solid var(--border-dark); transform: translateY(2.5px); }

    .goal-type-selector { display: flex; gap: 12px; margin-bottom: 16px; }
    /* Asymmetric padding (was a flat 8px) rather than transform — this
       button has its own visible border, and shifting the whole element
       would drag that border closer to .goal-input-group below it
       instead of just nudging the label. Splitting the same 16px total
       top+bottom padding as 10.5/5.5 moves the text down 2.5px while
       the border box itself — and the two tabs' shared height/
       alignment — stays exactly where it was. */
    .goal-type-btn { flex: 1; padding: 10.5px 8px 5.5px; border: 2px solid var(--border-dark); background: var(--bg-cream); font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: 700; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; cursor: pointer; border-radius: 4px; text-align: center; }
    .goal-type-btn.active { background: var(--border-dark); color: var(--paper-white); }

    .goal-input-group { margin-bottom: 12px; }
    .goal-input-label { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; display: block; transform: translateY(2.5px); }
    /* Same asymmetric-padding treatment as .goal-type-btn/the action
       buttons just below — brought up to parity now rather than left
       at its original flat 8px, so the actual typed number sits on the
       same nudged baseline as every other piece of text in this modal
       instead of being the one exception. */
    .goal-number-input { width: 100%; padding: 10.5px 12px 5.5px; border: 2px solid var(--border-dark); font-family: inherit; font-size: 1rem; border-radius: 4px; outline: none; background: var(--bg-cream); }

    /* The only ITC Avant Garde Gothic Book element in this modal, by
       design — every other piece of text here (title, toggles, input
       label, buttons) is explicitly set to Demi above/below. */
    .goal-research-note { font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.7rem; color: var(--text-muted); line-height: 1.4; margin-bottom: 16px; font-style: italic; transform: translateY(2.5px); }

    .goal-modal-actions { display: flex; gap: 12px; }
    .btn-action { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; padding: 8px 16px; border: 2px solid var(--border-dark); border-radius: 4px; font-weight: 700; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; cursor: pointer; }
    .btn-action.primary { background-color: var(--accent-orange); color: #FFFFFF; }
    #phrasing-save-key-btn.btn-action.primary { border: none; }
    .btn-action.secondary { background-color: transparent; color: var(--text-dark); }
    /* Mirrors .goal-type-selector/.goal-type-btn above: flex:1 on both
       buttons gives an equal 50/50 split whose flush outer edges match
       the input box's edges automatically (same 20px dropdown padding,
       no side margins). Flex's default stretch also equalizes height
       across the row, so a 2-line label (e.g. "Start Word Sprint")
       stretches Cancel to match rather than the reverse; the internal
       flex centering keeps each label centered within that shared height.
       Font-size bumped a step (0.75rem -> 0.9rem) so these two stand out
       more, same reasoning as the type tabs above for the padding split
       (10px/6px instead of a flat 8px) rather than transform: this is a
       flex-centered box, not a plain block, but the effect is the same —
       the fixed 16px top+bottom padding total shifts to 2px lower without
       moving the button's own border or its shared height with its
       sibling. The bump is deliberately modest specifically so "START
       WORD SPRINT" still wraps to two comfortably centered lines rather
       than crowding the button or forcing a wider layout. */
    .goal-modal-actions .btn-action { flex: 1; display: flex; align-items: center; justify-content: center; text-align: center; line-height: 1.3; padding-top: 10.5px; padding-bottom: 5.5px; font-size: 0.9rem; }

    /* ARCADE SPRINT COUNTDOWN — full-bleed cream field, no container
       card; the number/word itself IS the whole visual (Castle Crashers
       "All You Can Quaff"-style): a punchy elastic slam-in, a brief
       hold, then a fade/scale dissolve right as the next step slams in
       on top of it. */
    .countdown-overlay {
      position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
      background-color: var(--bg-cream); color: var(--text-dark);
      z-index: 500; display: flex; align-items: center; justify-content: center;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
    }
    .countdown-overlay.active { opacity: 1; pointer-events: auto; }
    .countdown-number {
      font-family: 'DXS Yagi Double', 'ITC Avant Garde Gothic Demi', sans-serif;
      font-size: 13rem; font-weight: 400;
      color: var(--text-dark); line-height: 1;
      /* Rests at invisible by default so a freshly-mounted step never
         flashes visible for a frame before its own animation class
         lands — see triggerCountdownSequence's remove/reflow/add
         retrigger dance. */
      opacity: 0;
    }
    /* NUMBERS (3, 2, 1) — slams in past its resting size with an
       elastic overshoot (2.6x down through 0.85x, a small rebound to
       1.08x, then settles), holds, then fades/scales back out right as
       the next number's own fresh slam-in begins. Retriggered fresh
       every step, always the full 1s of that step. */
    @keyframes countdown-slam {
      0%   { scale: 2.6; opacity: 0; }
      22%  { scale: 0.85; opacity: 1; }
      32%  { scale: 1.08; opacity: 1; }
      42%  { scale: 1; opacity: 1; }
      78%  { scale: 1; opacity: 1; }
      100% { scale: 1.35; opacity: 0; }
    }
    .countdown-number.countdown-slam { animation: countdown-slam 1s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
    /* FINALE ("WRITE!!") — a straight smack-down (`scale` alone, no
       bounce — see countdown-finale-scale below), then instead of
       holding still it shakes violently in place for the rest of its
       full 1s (`translate` alone, on a steps() timing so each offset
       SNAPS rather than eases into the next — the actual 16-bit
       displacement-glitch look, not a smooth wobble). Two separate
       transform-adjacent properties — not two animations both
       targeting the `transform` shorthand, which wouldn't compose; the
       later one would just win outright and silently cancel the
       other. */
    /* Unlike the numbers' countdown-slam, this has no undershoot/rebound
       bounce on landing — straight smack-down to scale 1, no elastic
       settle. Turns out that settle wobble was the actual source of the
       "gap" between smackdown and shake reported previously: it isn't a
       timing gap at all, it's the eye reading the bounce's own last
       wiggle as separate from the shake that follows it. Cut the bounce
       entirely instead of just tightening the gap. */
    @keyframes countdown-finale-scale {
      0%   { scale: 2.6; opacity: 0; }
      16%  { scale: 1; opacity: 1; }
      100% { scale: 1; opacity: 1; }
    }
    /* Shake picks up the INSTANT the smack-down lands (16%, matching
       countdown-finale-scale's own landing point exactly) rather than
       leaving a beat of stillness afterward. Landing moved in from 24%
       to 16% (a faster smack-down) specifically to give the shake more
       of the full 1s to run in, so the tick spacing below is re-spread
       across the now-larger 16%-100% window rather than left at its old
       spacing. */
    @keyframes countdown-finale-shake {
      0%, 16% { translate: 0 0; }
      24%  { translate: -7px 4px; }
      31%  { translate: 6px -5px; }
      39%  { translate: -5px -6px; }
      46%  { translate: 7px 5px; }
      54%  { translate: -6px 3px; }
      62%  { translate: 5px -4px; }
      69%  { translate: -7px -3px; }
      77%  { translate: 6px 6px; }
      85%  { translate: -4px -5px; }
      92%  { translate: 5px 4px; }
      100% { translate: 0 0; }
    }
    .countdown-number.countdown-finale {
      animation: countdown-finale-scale 1s cubic-bezier(0.2, 0.8, 0.2, 1) both,
                 countdown-finale-shake 1s steps(1, jump-end) both;
    }

    /* CONFETTI CANVAS */
    #confetti-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 300; }

    /* ============================================================
       STAGE 7 GRAND FINALE — overlay, mascot, speech box, spinner.
       All `.finale-anim` so html.finale-paused (set by the
       visibilitychange handler in the JS module) can pause every one of
       them uniformly — see that handler's own comment for why a single
       shared rule beats each animation needing its own pause class.
       ============================================================ */
    html.finale-paused .finale-anim { animation-play-state: paused; }

    /* Opaque the whole way, to the LEFT, fully clearing the viewport —
       no opacity fade (stays at 1 throughout; the previous version faded
       out mid-slide, which read as the UI dissolving rather than
       physically exiting). -115% guarantees it's traveled its own full
       width plus a margin past the left edge, not just partway off. */
    @keyframes finale-slide-out {
      from { transform: translateX(0); }
      to { transform: translateX(-115%); }
    }
    .workshop-wrapper.finale-sliding-out { animation: finale-slide-out 0.7s ease forwards; }

    /* Reveal and unveil both ride the same plain `transition: opacity` +
       .active toggle (add to reveal, remove to unveil) rather than two
       separate @keyframes animations — `animation` is a single CSS
       property, so a second animated class added later (finale-to-white,
       below) would silently win the cascade and cancel whichever opacity
       keyframe was "supposed" to still be holding this element visible.
       Splitting opacity onto `transition` and background-color onto
       `animation` means the two literally can't collide, since they're
       different properties. (This does mean these two ~0.5s fades aren't
       covered by the html.finale-paused pause mechanism the way the rest
       of the sequence is — an acceptable trade at the very open/close
       edges of a ~30s sequence, versus the alternative of fighting a real
       animation-property collision for it.) */
    .essay-finale-overlay {
      position: fixed; inset: 0; z-index: 405; background-color: var(--bg-cream);
      opacity: 0; pointer-events: none; overflow: hidden;
      transition: opacity 0.5s ease;
    }
    .essay-finale-overlay.active { opacity: 1; pointer-events: auto; }
    /* Unveil-only override of the base transition's 0.5s (see
       runEssayFinaleUnveilFadeStart) — the reveal at the start of the
       sequence keeps the base speed; this only ever gets added right
       before the fade back into the finished editor. Exactly
       FINALE_FADE_END_MS - FINALE_FADE_START_MS (a fixed, requested 2s
       duration, starting right at the waveform-measured onset of the
       outro fade — see FINALE_FADE_START_MS's own comment) so the
       fade's real duration matches those two exact timestamps
       precisely, not an approximation of the gap between them. */
    .essay-finale-overlay.finale-unveil-slow { transition-duration: 2s; }
    /* 0.6s to match finale-content-shift-down exactly below — both start
       on the beat drop's own frame, so matching durations is what makes
       them actually simultaneous rather than just triggered together. */
    @keyframes finale-overlay-to-white {
      from { background-color: var(--bg-cream); }
      to { background-color: #FFFFFF; }
    }
    .essay-finale-overlay.finale-to-white { animation: finale-overlay-to-white 0.6s ease forwards; }

    /* Plain positioning context now, not a centered flex column — the
       face and speech box each position themselves independently below
       (see their own comments) rather than being stacked flex siblings,
       since a flex column's total height (and therefore where its
       centered content sits) changes with the speech box's own content
       height — a multi-line dialogue line was visibly dragging the
       face's position around as text typed in, reported as the face
       "reacting" to the box's changing size. */
    .essay-finale-content {
      position: absolute; inset: 0;
    }
    /* The face slides DOWN (not up) as the background whites out — the
       illusion being that the VIEWPOINT is what's rising, past the face
       and up into the clouds, rather than the face itself flying off.
       120vh (not a fixed px offset) guarantees it clears the bottom of
       the viewport entirely regardless of screen size — "completely
       past the bottom," matching how the earlier UI slide-out travels a
       guaranteed-complete distance off the left edge rather than a
       partial dip. */
    @keyframes finale-content-shift-down { from { transform: translateY(0); } to { transform: translateY(120vh); } }
    .essay-finale-content.finale-shift-down { animation: finale-content-shift-down 0.6s ease-in forwards; }

    /* Opacity (entrance AND exit — the face also needs to actually
       disappear again once the whiteout starts, not just sit unseen
       behind the overlay's white background, since it renders as a DOM
       child stacked above that background) rides a plain transition off
       .active, same "different property than the animation below" split
       used on the overlay above. The entrance bounce is transform-only,
       so the two never compete for the same CSS property and can run
       concurrently. */
    /* Centered via left/top + negative margins (half its own fixed
       160x160 size), not transform:translate(-50%,-50%) — every
       animation below already owns `transform` for its own motion
       (bounce-in's translateY/scale, the notification bounce's
       translateY), and centering through a DIFFERENT property means
       positioning can never collide with whichever of those is active. */
    /* Base transform is the RESTING pose (translateY(0) scale(1)), not
       the off-screen pre-entrance one — hidden by opacity:0 until the
       entrance bounce plays anyway, so it's never visible here. That
       matters once beat drop removes every bounce class from the
       element (see runEssayFinaleBeatDrop): with nothing left to
       animate the cascade falls straight through to this base value, so
       it needs to already match wherever the just-finished bounce
       animation left off, or removing the classes would itself cause a
       jump/snap. finale-face-bounce-in's own 0% keyframe still starts
       off-screen regardless of this base value, so the entrance is
       unaffected. */
    .essay-finale-face {
      position: absolute; left: 50%; top: 50%; margin: -80px 0 0 -80px;
      width: 160px; height: 160px; opacity: 0; transform: translateY(0) scale(1);
      transition: opacity 0.3s ease;
    }
    .essay-finale-face svg { width: 100%; height: 100%; display: block; }
    .essay-finale-face .finale-face-edge { fill: var(--text-dark); }
    .essay-finale-face .finale-face-fill { fill: var(--paper-white); }
    .essay-finale-face .finale-face-eyes,
    .essay-finale-face .finale-face-mouth,
    .essay-finale-face .finale-face-cheeks { fill: var(--text-dark); }
    .essay-finale-face.active { opacity: 1; }
    @keyframes finale-face-bounce-in {
      0% { transform: translateY(-140px) scale(0.6); }
      55% { transform: translateY(20px) scale(1.08); }
      75% { transform: translateY(-8px) scale(0.97); }
      100% { transform: translateY(0) scale(1); }
    }
    .essay-finale-face.finale-face-bounce-in { animation: finale-face-bounce-in 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; }
    /* Expression changes themselves (party face, blush) are instant
       sprite swaps with no extra flourish — reads as a clean "same face,
       different expression" cut, matching "seamless facial transition,
       keep the head shape the same." The party phase's own continuous
       Dock-icon-style notification bounce (below) is a separate, actual
       motion the user asked for, not a swap flourish. */
    @keyframes finale-face-bounce-notify {
      0%, 24%, 100% { transform: translateY(0); }
      3% { transform: translateY(6px); }
      7% { transform: translateY(-58px); }
      11% { transform: translateY(4px); }
      13% { transform: translateY(0); }
      16% { transform: translateY(3px); }
      19% { transform: translateY(-28px); }
      22% { transform: translateY(2px); }
    }
    /* Deliberately lands on .finale-face-bounce-in's own `forwards`-held
       resting transform (translateY(0)) at 0%/24%/100% — the entrance
       bounce has already finished playing by the time the party phase
       starts it (see runEssayFinaleParty), so this simply takes over the
       same `animation` property with no jump at the handoff, the same
       "the two agree at the seam" trick used for the castle glow's
       fadein->pulse handoff. A FIXED 3 iterations (not infinite, cut off
       from outside by a timer) — same idea as the confetti/fireworks
       spawn-then-let-finish treatment: it completes whole cycles and
       rests (forwards holds translateY(0), its own resting frame) on
       its own, so there's no risk of a timer landing mid-arc and
       stopping it looking choppy — see runEssayFinaleParty for how the
       party hold itself is sized to comfortably outlast this. */
    .essay-finale-face.finale-face-bouncing { animation: finale-face-bounce-notify 1.2s ease-in-out 3 forwards; }

    /* Positioned independently of the face (left/top + translate, its
       own thing entirely — see .essay-finale-content's own comment),
       centered horizontally and sitting at 75% down the viewport,
       "smack dab in between the middle of the screen and the bottom."
       A genuinely FIXED height (not min-height) + overflow:hidden, with
       JS (typeFinaleLine) keeping it scrolled to the bottom on every
       character — a real retro-RPG scrolling text window: once typed
       content grows past what the box shows, earlier text scrolls up
       and out rather than the box itself growing taller. ~3 lines' worth
       at the current font/line-height. Width brought back down from an
       earlier, much bigger font's own wider box — this one doesn't need
       nearly as much horizontal room. */
    .essay-finale-speech-box {
      position: absolute; left: 50%; top: 75%;
      background: #FFFFFF; border: 3px solid #000000; box-shadow: 6px 6px 0px #000000;
      box-sizing: border-box;
      width: min(620px, 85vw); height: 200px; padding: 24px 30px;
      overflow: hidden;
      opacity: 0; transform: translate(-50%, -50%) scale(0.92);
      transition: opacity 0.15s ease, transform 0.15s ease;
      display: flex; align-items: center; justify-content: center; text-align: center;
    }
    .essay-finale-speech-box.active { opacity: 1; transform: translate(-50%, -50%) scale(1); }
    .essay-finale-speech-text {
      font-family: 'Pixelify Sans', 'Georgia', serif; font-weight: 400; font-size: 2.05rem;
      line-height: 1.5; color: #000000; white-space: pre-wrap;
    }

    .essay-finale-confetti-canvas { position: absolute; inset: 0; pointer-events: none; }

    /* Retro 8-bit loading bar shown briefly during the white-hold, between
       the beat drop's whiteout and the fade back into the editor (see
       FINALE_LOADING_BAR_SHOW_MS/HIDE_MS). Black-at-low-opacity rather
       than a literal gray color — that's what "translucent light gray
       against pure white" actually means: the alpha blend against the
       white background IS the gray, not a separate color choice. No
       rounding/blur anywhere (no border-radius, no shadow) so it stays
       genuinely hard-edged/pixelated at this small size. */
    .essay-finale-loading-bar {
      position: absolute; left: 50%; top: 50%; width: 140px; height: 24px;
      transform: translate(-50%, -50%); opacity: 0; pointer-events: none;
    }
    .essay-finale-loading-bar.active { opacity: 1; }
    /* A plain 4x vertical upscale of the original 1px-border/1px-gap/2px-
       fill graphic (6px total) — every vertical measurement scaled up
       together (border, gap, fill height all 4x), not just the fill
       alone, so the same 1:1:2 proportions hold at the new size instead
       of the gap/border reading thin next to a suddenly-thick fill.
       Width is untouched — this was reported as "horizontal length is
       fine," a vertical-only thickness complaint. 4px border + 4px
       padding (the "gap") + an 8px-tall fill child exactly filling
       what's left (24 - 2*4 border - 2*4 padding = 8px). */
    .essay-finale-loading-bar-frame {
      width: 100%; height: 100%; box-sizing: border-box;
      border: 4px solid rgba(0, 0, 0, 0.35); padding: 4px;
    }
    .essay-finale-loading-bar-fill {
      height: 8px; background: rgba(0, 0, 0, 0.35);
      /* Width is set inline per step (33% / 50% / 75%) — no transition,
         since a hard instant jump between steps is the "choppy, stepped
         8-bit" look this was asked for, not a smooth fill animation. */
    }

    /* CHOOSE YOUR FORMAT MODAL */
    .format-modal {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.96);
      width: 460px; max-width: 90vw;
      background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 10px;
      box-shadow: 6px 6px 0px var(--border-dark); z-index: 401;
      opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s;
      padding: 26px 28px 30px;
    }
    .format-modal.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
    .import-dropzone {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 12px; padding: 30px; text-align: center; cursor: pointer;
      border: 3px dashed #CCC8C0; border-radius: 8px;
      transition: border-color 0.15s, background-color 0.15s;
    }
    .import-dropzone.drag-over { border-color: var(--accent-orange); background-color: rgba(217, 83, 30, 0.06); }
    .import-dropzone svg { width: 34px; height: 34px; fill: var(--text-muted); }
    .import-mla-toggle {
      display: flex; align-items: center; gap: 8px; margin-top: 16px;
      font-size: 0.82rem; font-weight: 600; cursor: pointer;
    }
    .import-format-note { font-size: 0.7rem; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
    /* Shared by both "Import Document" and "Choose Your Format" — both
       want Souvenir Medium now, so this is the base rule rather than a
       per-modal override. */
    .format-modal-title {
      font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1.3rem; font-weight: bold;
      text-align: center; margin-bottom: 20px;
    }
    .format-choice-row { display: flex; gap: 18px; }
    .format-choice-card {
      flex: 1; border: 2px solid var(--border-dark); border-radius: 6px;
      background: var(--bg-card); cursor: pointer; padding: 14px 10px 12px;
      display: flex; flex-direction: column; align-items: center; gap: 10px;
      transition: transform 0.15s ease, box-shadow 0.15s ease;
    }
    .format-choice-card:hover { transform: translateY(-2px); box-shadow: 3px 3px 0px var(--border-dark); }
    .format-choice-card.picked { transform: scale(1.06); box-shadow: 3px 3px 0px var(--accent-orange); border-color: var(--accent-orange); }
    .format-preview {
      width: 100%; height: 92px; background: var(--paper-white); border: 1px solid #CCC8C0;
      border-radius: 2px; padding: 10px 12px; box-sizing: border-box;
    }
    .format-preview .fp-line { height: 5px; background: #D8D4CC; border-radius: 1px; margin-bottom: 6px; }
    .format-preview.fp-normal .fp-title { width: 55%; height: 8px; margin: 0 auto 10px; background: #9a958c; border-radius: 1px; }
    .format-preview.fp-mla .fp-title { width: 40%; height: 5px; margin: 0 0 8px; background: #9a958c; border-radius: 1px; }
    .format-preview.fp-mla .fp-line { width: 92%; margin-left: 8px; }
    .format-preview .fp-line:nth-child(3) { width: 80%; }
    .format-choice-label { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
    .format-choice-sub { font-size: 0.65rem; color: var(--text-muted); text-align: center; line-height: 1.3; }

    /* MULTI-SELECT DOCUMENT GRID */
    .doc-card.select-mode-active { cursor: pointer; }
    .doc-select-checkbox {
      position: absolute; top: 10px; left: 10px; width: 20px; height: 20px;
      border-radius: 50%; border: 2px solid var(--border-dark); background: var(--paper-white);
      display: flex; align-items: center; justify-content: center; font-size: 0.7rem;
      z-index: 5;
    }
    .doc-card.doc-selected { box-shadow: 3px 3px 0px var(--accent-orange); border-color: var(--accent-orange); }
    .doc-card.doc-selected .doc-select-checkbox { background: var(--accent-orange); border-color: var(--accent-orange); color: #FFF; }
    .bulk-action-bar {
      display: flex; align-items: center; gap: 10px;
      background: var(--bg-card); border-radius: 6px; border: 0px solid var(--border-dark);
      padding: 0; opacity: 0; max-width: 0; overflow: hidden; pointer-events: none;
      transition: opacity 0.3s ease, max-width 0.3s ease, padding 0.3s ease, border-width 0.3s ease;
      white-space: nowrap;
    }
    .bulk-action-bar.active {
      opacity: 1; max-width: 900px; padding: 8px 14px; pointer-events: auto;
      border-width: 2px; flex-grow: 1;
      /* Scrollable only once content genuinely doesn't fit (a narrow
         window squeezing this bar below its natural content width) —
         native overflow-x:auto shows a scrollbar exactly when needed and
         nothing otherwise, rather than the previous overflow:hidden simply
         clipping CANCEL out of reach with no way to get to it. */
      overflow-x: auto; overflow-y: hidden;
    }
    .bulk-action-bar.active::-webkit-scrollbar { height: 4px; }
    .bulk-action-bar.active::-webkit-scrollbar-track { background: transparent; }
    .bulk-action-bar.active::-webkit-scrollbar-thumb { background-color: rgba(34, 34, 34, 0.2); border-radius: 4px; }
    .bulk-count { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-right: auto; }
    .bulk-btn {
      background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 4px;
      /* Asymmetric padding (was a flat 6px) — same technique as
         .btn-empty-trash above: splitting the same 12px total top+bottom
         padding as 7.5/4.5 nudges the text down 1.5px to sit centered
         and level with "Cancel" beside it, without moving the border
         box itself (a plain transform would drag the border down too). */
      padding: 7.5px 10px 4.5px; font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
      letter-spacing: 0.5px; cursor: pointer; transition: 0.12s;
    }
    .bulk-btn:hover { background: var(--border-dark); color: var(--paper-white); }
    .bulk-btn.danger:hover { background: var(--accent-orange); border-color: var(--accent-orange); }

    .status-toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: var(--border-dark); color: var(--paper-white); padding: 6px 14px; border-radius: 4px; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; pointer-events: none; opacity: 0; transition: 0.2s; z-index: 350; }
    .status-toast.show { opacity: 1; }
    /* While the editor is open, the paper page leaves a slim strip of blank
       space between its bottom edge and the viewport edge (wrapper padding +
       centering gap). Drop the toast into that strip instead of the default
       viewport-centered position, and shrink it slightly so it comfortably
       fits without touching the paper. Only applies when #editor-view is
       visible (not .hidden); the grid view keeps the original placement. */
    #editor-view:not(.hidden) ~ .status-toast {
      bottom: 8px;
      padding: 4px 12px;
      font-size: 0.62rem;
    }
    /* Bring the toast near the stage card (the "action") while in the
       workshop, instead of the far bottom edge — easier to notice mid-flow
       without having to glance all the way down. This is a fallback only:
       JS sets an inline top position tracking the actual card's bottom
       edge (its height varies by stage/info-panel state), which overrides
       this whenever a card is present. */
    #workshop-view:not(.hidden) ~ .status-toast {
      bottom: auto;
      top: 118px;
      font-size: 0.8rem;
      padding: 8px 18px;
    }

    /* ============================================================
       BUILD-AN-ESSAY WORKSHOP
       Its own accent (--workshop-blue/-dark/-cyan, defined in :root),
       its own view (like #menu-view / #editor-view), left untouched
       everywhere else in the app. Retro-pixel corner treatment (a
       stair-step clip-path) is used sparingly on the node badges and
       the DONE button as the one signature "video game" touch, rather
       than applied everywhere.
       ============================================================ */
    .pixel-corners {
      clip-path: polygon(
        0% 8px, 8px 8px, 8px 0%, calc(100% - 8px) 0%, calc(100% - 8px) 8px, 100% 8px,
        100% calc(100% - 8px), calc(100% - 8px) calc(100% - 8px), calc(100% - 8px) 100%,
        8px 100%, 8px calc(100% - 8px), 0% calc(100% - 8px)
      );
    }
    #format-modal { width: 460px; }

    /* DOCUMENT CENTER NAV TREE — the left-edge, faint-until-hover vertical
       nav that moves between whole "experiences" (Document Center,
       Build-an-Essay, and eventually the Essay Scanner), replacing the
       earlier idea of Build-an-Essay as a third format-picker card. Lives
       inside #menu-view (nested, not a page-level sibling) so it hides
       for free whenever #menu-view does. */
    .docnav-tree {
      position: fixed; top: 50%; left: 26px; transform: translateY(-50%);
      display: flex; flex-direction: column; align-items: flex-start; gap: 0;
      z-index: 60;
      /* Declared here unconditionally (not inside the @media block below)
         so it stays in effect across the breakpoint change — a property
         with a transition already running smoothly interpolates to
         whatever new value a media query gives it, rather than the change
         snapping instantly. Covers the wide<->narrow layout switch itself,
         which otherwise reads as the tree just vanishing and reappearing
         elsewhere rather than actually moving there. flex-direction and
         background can't meaningfully interpolate (discrete values), so
         those two aspects of the swap still snap — but the position
         change, which is the part that reads as "disappearing," doesn't. */
      transition: top 0.3s ease, left 0.3s ease, right 0.3s ease, padding 0.3s ease;
    }
    .docnav-item {
      display: flex; align-items: center; gap: 12px; cursor: pointer;
      padding: 8px 0; background: none; border: none; text-align: left;
      color: var(--text-muted); font-family: 'ITC Avant Garde Gothic Demi', -apple-system, BlinkMacSystemFont, sans-serif;
      font-size: 0.74rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
      /* Overrides the app-wide body line-height (1.5) — at that value the
         text's line-box is considerably taller than its actual cap-height
         ink, so flex's align-items:center was centering against the
         inflated line-box rather than the visible glyphs, leaving the
         label looking vertically off-center against the compact dot next
         to it. A tight line-height brings the line-box back down close to
         the real ink height so the centering reads correctly. */
      line-height: 1;
      transition: color 0.15s;
    }
    .docnav-item:hover:not(.disabled) { color: var(--text-dark); }
    .docnav-item.current { color: var(--text-dark); }
    .docnav-item.disabled { cursor: default; opacity: 0.6; }
    /* Build-an-Essay specifically gets a touch of "faint until you look at
       it" back — everything else in the tree stays fully visible. */
    /* A short delay before the fade-OUT starts (not the fade-in) absorbs
       brief hover-boundary jitter — without it, a cursor that wobbles
       near the element's edge keeps re-triggering :hover on/off, which
       restarts this transition from scratch each time. Confirmed directly
       by sampling actual video frames: opacity was sitting dead flat for
       9 straight frames mid-fade, then completing the rest of the fade in
       a single frame — the signature of a transition being restarted
       repeatedly rather than genuinely animating slowly. The delay only
       applies to leaving, so hovering in still feels instant. */
    /* Matches the word-count widget's fade exactly: a plain, single-
       property transition driven by a JS-toggled class, not the native
       :hover pseudo-class. That distinction is the actual fix — :hover
       recomputes (and restarts) the transition on every boundary crossing,
       and a wobbling cursor near the element's edge can cross that
       boundary many times a second, which is what produced the choppy
       stop-start fade even with a transition-delay layered on top.
       mouseenter/mouseleave with a small debounce (below, in JS) can't
       restart mid-flight the same way, since leaving briefly and
       re-entering cancels the pending removal instead of toggling the
       class at all. */
    /* Also drives color from the same JS-toggled class, not the native
       :hover pseudo-class the general .docnav-item rule above uses.
       That mismatch was the actual "three stages" bug: native :hover ends
       the instant the cursor crosses the boundary, reverting color right
       away, while the debounced opacity class stayed active for another
       100ms — so leaving briefly showed color already muted but opacity
       still bright, a real (not imagined) intermediate visual state
       between "hovering" and "at rest." Both properties now move under
       the exact same trigger, so there's nothing left to fall out of
       sync. */
    /* Resting state only — the hover animation itself is now driven by
       the Web Animations API in JS (setDocnavEssayHoverState), not a CSS
       transition, so no transition/.hover-active pair is declared here. */
    .docnav-item.essay { opacity: 0.78; }
    /* Each destination gets its own permanent, always-visible color —
       "current" is conveyed through text color (below) only, not by
       whether the dot happens to be filled, since you can't see the dot
       change while you're actually on that page anyway. */
    .docnav-dot {
      width: 10px; height: 10px; border-radius: 50%;
      flex-shrink: 0; box-sizing: border-box;
    }
    /* Avant Garde Demi's ink isn't symmetric within its line-box even at
       line-height:1 — it sits optically high relative to the true
       geometric center of the (perfectly circular, unaffected) dot next
       to it. Now that the label is its own element (.docnav-label) this
       nudges the text down directly, rather than nudging the dot up to
       fake the same correction from the other side. */
    .docnav-label { display: inline-block; transform: translateY(1.5px); }
    .docnav-item.documents .docnav-dot { background: var(--accent-orange); }
    .docnav-item.essay .docnav-dot { background: var(--workshop-blue); }
    .docnav-item.scanner .docnav-dot { background: var(--write-burgundy); }
    .docnav-line { width: 1px; height: 26px; background: var(--text-muted); opacity: 0.35; margin-left: 5px; }

    /* Below ~1340px, the fixed-width centered menu-container's left edge
       creeps in far enough to collide with the nav tree sitting in the
       page's left margin (verified directly: menu-container is a fixed
       1000px wide and centers via margin:auto, so its left edge is
       (viewport-1000)/2 — comfortably clear of the tree above ~1340px,
       overlapping the search bar and doc cards below it). Rather than
       trying to out-narrow the collision by nudging position, switch the
       tree to a horizontal bar docked under the header instead — it no
       longer depends on how much left margin remains at all, so this
       holds at any narrower width, not just down to some second
       threshold. */
    @media (max-width: 1340px) {
      .docnav-tree {
        position: fixed; top: 0; left: 0; right: 0; transform: none;
        flex-direction: row; align-items: center; justify-content: center;
        flex-wrap: wrap; gap: 18px; padding: 10px 16px;
        background: var(--bg-cream); border-bottom: 1px solid rgba(34, 34, 34, 0.12);
        z-index: 90;
      }
      .docnav-line { width: 1px; height: 14px; margin-left: 0; }
      /* The header is fixed here too (see its own base rule) — stacked
         directly below the now-horizontal nav bar instead of at top:0,
         so the two don't overlap. .menu-content-wrapper's compensating
         padding grows to match both bars' combined height (46 + 96). */
      header { top: 46px; }
      #menu-view .menu-content-wrapper { padding-top: 142px; display: block; }
      /* This layout's resting transform is none (horizontal bar), not the
         wide-layout's translateY(-50%) — the plummet/rise keyframes need
         their own version here too, or the tree would snap to the wrong
         baseline the instant the animation starts. Declared after the
         wide-layout @keyframes of the same name further down, so —
         correctly — this version only takes over once this media query is
         actually active. */
      @keyframes docnav-plummet-out-narrow { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-70vh); opacity: 0.25; } }
      @keyframes docnav-rise-in-narrow { 0% { transform: translateY(-40vh); opacity: 0; } 70% { transform: translateY(2vh); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } }
      /* animation-name override for this breakpoint lives further down,
         after the base .docnav-tree.plummet-out/.rise-in rules — same
         specificity, so it has to come later in the stylesheet to
         actually win the cascade at narrow widths. */
    }

    /* PLUMMET / RISE — the vertical "falling into a different experience"
       transition between Document Center and the Build-an-Essay hub.
       Deliberately distinct from the horizontal swing-in used between
       stages inside the workshop: this one marks "you just left the
       document center entirely," that one marks "next chunk of the essay." */
    @keyframes workshop-plummet-out { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-70vh); opacity: 0.25; } }
    @keyframes workshop-plummet-in { 0% { transform: translateY(40vh); opacity: 0; } 70% { transform: translateY(-2vh); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } }
    @keyframes workshop-rise-out { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(70vh); opacity: 0.25; } }
    @keyframes workshop-rise-in { 0% { transform: translateY(-40vh); opacity: 0; } 70% { transform: translateY(2vh); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } }
    .plummet-out { animation: workshop-plummet-out 0.5s cubic-bezier(0.55, 0, 0.85, 0.35) forwards; }
    .plummet-in { animation: workshop-plummet-in 0.55s cubic-bezier(0.16, 1, 0.3, 1) both; }
    .rise-out { animation: workshop-rise-out 0.5s cubic-bezier(0.55, 0, 0.85, 0.35) forwards; }
    .rise-in { animation: workshop-rise-in 0.55s cubic-bezier(0.16, 1, 0.3, 1) both; }
    /* .docnav-tree's resting position already carries its own
       translateY(-50%) for vertical centering — the animations above
       assume a resting translateY(0), so reusing them as-is on the tree
       would snap away that -50% the instant the animation starts (a real,
       visible jump). Same motion, but baked around the tree's actual
       resting transform instead, so it travels away WITH the rest of the
       UI rather than sitting still and then just vanishing when the view
       underneath it gets swapped out. */
    @keyframes docnav-plummet-out { 0% { transform: translateY(-50%); opacity: 1; } 100% { transform: translateY(calc(-50% - 70vh)); opacity: 0.25; } }
    @keyframes docnav-rise-in { 0% { transform: translateY(calc(-50% - 40vh)); opacity: 0; } 70% { transform: translateY(calc(-50% + 2vh)); opacity: 1; } 100% { transform: translateY(-50%); opacity: 1; } }
    .docnav-tree.plummet-out { animation: docnav-plummet-out 0.5s cubic-bezier(0.55, 0, 0.85, 0.35) forwards; }
    .docnav-tree.rise-in { animation: docnav-rise-in 0.55s cubic-bezier(0.16, 1, 0.3, 1) both; }
    /* Overrides just the animation-name at the narrow breakpoint, rather
       than redeclaring @keyframes under the same name inside vs. outside
       a media query — confirmed directly that same-name @keyframes don't
       reliably respect the media condition the way ordinary style rules
       do (the wide-layout version, despite being wrapped differently,
       kept winning regardless of viewport — it's the LAST declaration of
       that name in the whole stylesheet that wins, not the last one whose
       own condition happens to match). Distinct keyframe names sidestep
       the ambiguity entirely; this just needs to come after the base
       declaration above so it wins the ordinary cascade at this width. */
    @media (max-width: 1340px) {
      .docnav-tree.plummet-out { animation-name: docnav-plummet-out-narrow; }
      .docnav-tree.rise-in { animation-name: docnav-rise-in-narrow; }
    }

    /* TITLE SCREEN — the video-game-style landing pad inside the workshop
       experience, shown before a specific essay document is chosen. */
    .workshop-title-screen {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      flex-grow: 1; width: 100%; text-align: center; gap: 22px;
    }
    .workshop-title-logo {
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: 400; font-size: 2.35rem;
      color: var(--workshop-blue); letter-spacing: 1px;
      text-shadow: 4px 4px 0px var(--workshop-cyan);
    }
    .workshop-title-tagline {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.8rem;
      color: var(--text-muted); text-transform: uppercase; letter-spacing: 2px; margin-top: -10px;
    }
    .workshop-title-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }
    .workshop-title-btn {
      background: var(--workshop-blue); color: #FFFFFF; border: 2px solid var(--border-dark);
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 13px;
      padding: 16px 36px; cursor: pointer; box-shadow: 3px 3px 0px var(--border-dark);
      text-transform: uppercase; letter-spacing: 1.5px; transition: transform 0.1s ease;
      animation: workshop-title-blink 1.6s ease-in-out infinite;
      display: flex; align-items: center; justify-content: center;
    }
    .workshop-title-btn:active { transform: translateY(2px); box-shadow: 1px 1px 0px var(--border-dark); }
    .workshop-title-btn.secondary {
      background: var(--paper-white); color: var(--workshop-blue); animation: none;
    }
    @keyframes workshop-title-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.75; } }

    /* "Launch" sequence for NEW ESSAY — a brief, deliberately more
       dramatic moment than any other transition in the app, reserved for
       this one "we're really starting now" beat. The logo bursts outward
       and fades while a radial flash pulses across the screen; the actual
       stage doesn't render until this finishes playing. */
    @keyframes workshop-launch-burst {
      0% { transform: scale(1); opacity: 1; text-shadow: 4px 4px 0px var(--workshop-cyan); }
      40% { transform: scale(1.18); opacity: 1; text-shadow: 0 0 24px var(--workshop-cyan); }
      100% { transform: scale(1.55); opacity: 0; text-shadow: 0 0 40px var(--workshop-cyan); }
    }
    .workshop-title-logo.launching { animation: workshop-launch-burst 0.45s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
    @keyframes workshop-flash-pulse { 0% { opacity: 0; } 25% { opacity: 0.4; } 100% { opacity: 0; } }
    .workshop-launch-flash {
      position: fixed; inset: 0; z-index: 350; pointer-events: none;
      background: radial-gradient(circle, var(--workshop-cyan) 0%, transparent 70%);
      animation: workshop-flash-pulse 0.45s ease-out forwards;
    }
    /* The map + first stage card fade in together right as the launch
       flash/burst finishes, rather than appearing instantly the moment
       title-mode is switched off — connects the "launch" moment to the
       actual content landing instead of the content just popping in
       separately after. */
    @keyframes workshop-content-fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
    .workshop-content-fade-in { animation: workshop-content-fade-in 0.5s ease forwards; }
    /* Title screen and the map/stage UI are mutually exclusive states of
       the same view — toggled by a single class on .workshop-wrapper so
       there's no chance of both being visible (or neither) at once. */
    .workshop-title-screen { display: none; }
    .workshop-wrapper.title-mode .workshop-title-screen { display: flex; }
    .workshop-wrapper.title-mode .workshop-map,
    .workshop-wrapper.title-mode #workshop-stage-host,
    .workshop-wrapper.title-mode .workshop-word-total-wrapper,
    /* Stage 6's drawer is static HTML, never torn down between visits —
       unlike the stage card (rebuilt fresh every renderWorkshopStage
       call), whatever hidden/open state it was left in when the workshop
       was last closed mid-Stage-6 otherwise just sits there, still
       visible, still occupying its old position, right on top of the
       title screen on the next visit (confirmed: this is also what was
       quietly breaking the tagline's fade — it wasn't failing to
       animate, it was animating fine underneath an opaque drawer that
       had no business still being there). */
    .workshop-wrapper.title-mode #workshop-drawer {
      display: none;
    }

    .workshop-wrapper {
      position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
      background-color: var(--bg-cream); z-index: 100;
      display: flex; flex-direction: column; align-items: center;
      padding: 30px 20px 20px; overflow-y: auto;
      /* Explicit rather than relying on the "visible + non-visible sibling
         axis computes to auto" quirk that overflow-y:auto alone already
         triggers — this is what lets the page scroll horizontally on a
         narrow window when Stage 6's drawer (position:absolute, so it
         genuinely contributes to this element's own scrollable overflow)
         is pulled fully open and doesn't fit in the left margin. Inert
         (no visible scrollbar, nothing to scroll) whenever everything
         actually fits, which is the overwhelming majority of the time. */
      overflow-x: auto;
      transition: padding-right 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    }
    /* Same shift-left-when-a-panel-opens treatment as .editor-wrapper —
       identical padding-right amounts, so a panel opening shifts the
       workshop content over by exactly the same amount it shifts the
       text editor, for consistency between the two modes. */
    .workshop-wrapper.thesaurus-open {
      padding-right: 360px;
    }
    .workshop-wrapper.write-open {
      padding-right: 520px;
    }
    /* Own class rather than reusing .write-open — see the comment on
       openTransitionsPanel for why sharing it with the MLA/write panel
       caused real cross-talk. Same 520px, since #transitions-panel is
       built on the same .write-panel base (480px wide, 520px off-screen
       throw) as the MLA drawer. */
    .workshop-wrapper.transitions-open {
      padding-right: 520px;
    }
    /* Build-an-Essay's Grammar bubble opens a narrower, grammar-only
       variant of the panel (see .write-panel.grammar-only below) — the
       shift needs to match that narrower width, not the full 520px. */
    .workshop-wrapper.write-open-narrow, .editor-wrapper.write-open-narrow {
      padding-right: 360px;
    }
    .workshop-exit-btn {
      position: fixed; top: 16px; left: 20px; z-index: 110;
    }
    .workshop-wrapper.title-mode .workshop-exit-btn { display: none; }

    /* CUSTOM QUIT CONFIRMATION — replaces the browser's native confirm()
       with something that actually matches the retro aesthetic. Only ever
       shown mid-stage; the title screen's own EXIT button skips
       confirmation entirely (nothing is "in danger" of being lost from a
       save-select screen the way it might feel mid-sentence). */
    .workshop-quit-overlay {
      position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 400;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
    }
    .workshop-quit-overlay.active { opacity: 1; pointer-events: auto; }
    .workshop-quit-modal {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.94);
      background: var(--paper-white); border: 3px solid var(--border-dark);
      box-shadow: 6px 6px 0px var(--border-dark); z-index: 401;
      padding: 26px 30px; width: 340px; max-width: 88vw; text-align: center;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease;
    }
    .workshop-quit-modal.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
    .workshop-quit-title {
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 1rem;
      color: var(--text-dark); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px;
    }
    .workshop-quit-sub {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.78rem;
      color: var(--text-muted); margin-bottom: 20px;
    }
    .workshop-quit-actions { display: flex; gap: 12px; }
    .workshop-quit-btn {
      flex: 1; padding: 12px 0; border: 2px solid var(--border-dark); cursor: pointer;
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 11px;
      text-transform: uppercase; letter-spacing: 1px; box-shadow: 2px 2px 0px var(--border-dark);
      transition: transform 0.1s ease;
      display: flex; align-items: center; justify-content: center;
    }
    .workshop-quit-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(--border-dark); }
    /* Arcade Red — EXIT is the destructive/leaving action here. CANCEL
       stays neutral (paper/cream, matching the app's default button
       chrome) since it's the safe, do-nothing choice. */
    .workshop-quit-btn.exit { background: #D32F2F; color: #FFFFFF; border-color: var(--border-dark); }
    .workshop-quit-btn.cancel { background: var(--bg-cream); color: var(--text-dark); }

    /* STAGE 7 GRAND FINALE — confirm gate. Same hard-outline/drop-shadow
       family as .workshop-quit-modal just above (copied treatment, not a
       shared class, since the copy/button count/colors all differ), just
       a touch wider with a smaller title size so the longer confirm
       sentence wraps cleanly instead of overflowing at the quit modal's
       narrower 340px/1rem. */
    .essay-finale-confirm-overlay {
      position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 402;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
    }
    .essay-finale-confirm-overlay.active { opacity: 1; pointer-events: auto; }
    .essay-finale-confirm-modal {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.94);
      background: var(--paper-white); border: 3px solid var(--border-dark);
      box-shadow: 6px 6px 0px var(--border-dark); z-index: 403;
      padding: 26px 30px; width: 380px; max-width: 88vw; text-align: center;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease;
    }
    .essay-finale-confirm-modal.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
    .essay-finale-confirm-title {
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 0.85rem; line-height: 1.6;
      color: var(--text-dark); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px;
    }
    .essay-finale-confirm-sub {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-style: italic; font-size: 0.78rem;
      color: var(--text-muted); margin-bottom: 20px;
    }
    .essay-finale-confirm-actions { display: flex; gap: 12px; }
    .essay-finale-confirm-btn {
      flex: 1; padding: 10px 0; border: 2px solid var(--border-dark); cursor: pointer;
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 10px;
      text-transform: uppercase; letter-spacing: 1px; box-shadow: 2px 2px 0px var(--border-dark);
      transition: transform 0.1s ease;
      display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1.8;
    }
    .essay-finale-confirm-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(--border-dark); }
    .essay-finale-confirm-btn.yes { background: #2E8B4F; color: #FFFFFF; border-color: var(--border-dark); }
    .essay-finale-confirm-btn.wait { background: #D32F2F; color: #FFFFFF; border-color: var(--border-dark); }

    /* Hard-stop lockout — DONE! on Intro & Conclusion while citations are
       still missing. Same hard-outline/drop-shadow modal family as
       .workshop-quit-modal, but only one way out (a single full-width
       button), since there's no real second choice here the way EXIT/
       CANCEL or YES/WAIT offer one. */
    .essay-plagiarism-overlay {
      position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 404;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
    }
    .essay-plagiarism-overlay.active { opacity: 1; pointer-events: auto; }
    .essay-plagiarism-modal {
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.94);
      background: var(--paper-white); border: 3px solid var(--border-dark);
      box-shadow: 6px 6px 0px var(--border-dark); z-index: 405;
      padding: 26px 30px; width: 360px; max-width: 88vw; text-align: center;
      opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease;
    }
    .essay-plagiarism-modal.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
    /* The "briefly vibrates/shakes" physical-emphasis cue — a separate
       one-shot animation on the SAME transform property .active already
       transitions, but that's not a collision the way stacked classes
       elsewhere in this file have been: an animation always overrides a
       transition on the same property while it's actively running (not
       two class rules silently fighting over `animation` itself), and
       every keyframe stop here bakes in the exact same translate(-50%,
       -50%) centering .active already settled on, so there's nothing to
       jump to when the animation ends and the transition's steady-state
       value takes back over. */
    .essay-plagiarism-modal.shaking { animation: essay-plagiarism-shake 0.5s ease; }
    @keyframes essay-plagiarism-shake {
      0%, 100% { transform: translate(-50%, -50%) translateX(0); }
      15% { transform: translate(-50%, -50%) translateX(-10px); }
      30% { transform: translate(-50%, -50%) translateX(9px); }
      45% { transform: translate(-50%, -50%) translateX(-7px); }
      60% { transform: translate(-50%, -50%) translateX(5px); }
      75% { transform: translate(-50%, -50%) translateX(-3px); }
      90% { transform: translate(-50%, -50%) translateX(1px); }
    }
    .essay-plagiarism-title {
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 0.95rem;
      color: #D32F2F; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px;
    }
    .essay-plagiarism-sub {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.78rem;
      color: var(--text-muted); margin-bottom: 20px;
    }
    .essay-plagiarism-btn {
      width: 100%; padding: 12px 0; border: 2px solid var(--border-dark); cursor: pointer;
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 11px;
      text-transform: uppercase; letter-spacing: 1px; box-shadow: 2px 2px 0px var(--border-dark);
      transition: transform 0.1s ease;
      display: flex; align-items: center; justify-content: center;
      background: var(--bg-cream); color: var(--text-dark);
    }
    .essay-plagiarism-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(--border-dark); }

    .workshop-save-slots {
      display: flex; flex-direction: column; width: 100%; max-width: 380px;
      max-height: 260px; overflow-y: auto; margin: 0 auto;
      /* Spacing between slots comes from each slot's own margin-bottom
         (not a container gap) so a deleting slot's collapse animation can
         smoothly close up its own trailing space too, instead of leaving
         a fixed gap behind that only disappears in an abrupt final jump. */
      /* Reserve room on the right for the hover shift + box-shadow below,
         so the slot's hover motion happens entirely inside space this
         container already claimed — rather than growing past it and
         getting clipped by the viewport edge on narrow screens. */
      padding: 2px 8px 2px 2px; box-sizing: border-box;
    }
    .workshop-save-slot {
      display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
      background: var(--paper-white); border: 2px solid var(--border-dark);
      padding: 12px 16px; cursor: pointer; text-align: left;
      box-shadow: 2px 2px 0px var(--border-dark);
      font-family: 'ITC Avant Garde Gothic Book', sans-serif;
      width: 100%; box-sizing: border-box; margin-bottom: 10px; position: relative;
      /* Overrides the app-wide body line-height (1.5) — at that value the
         three stacked text rows (title/snippet/meta) no longer fit their
         allotted space inside this fixed-height card, so flex shrank the
         snippet row below its own line-height and its own
         overflow:hidden (meant for the horizontal ellipsis truncation)
         clipped straight through the middle of the line's glyphs instead
         — a vertical clip, not the intended horizontal one. A tighter
         line-height restores enough room for every row to render at its
         natural, un-shrunk height. */
      line-height: 1.3;
      /* Fixed rather than content-based — every slot's title/snippet/meta
         are already single-line + ellipsis-truncated, so this just keeps
         every card the exact same size regardless of which draft it is,
         rather than letting shorter content (like "No thesis written
         yet") render a visibly smaller card than its neighbors. */
      height: 78px;
      transition: transform 0.1s ease, height 0.3s ease, margin-bottom 0.3s ease,
        padding-top 0.3s ease, padding-bottom 0.3s ease, border-width 0.3s ease, opacity 0.3s ease;
    }
    .workshop-save-slot:last-child { margin-bottom: 0; }
    .workshop-save-slot:hover { transform: translateX(2px); }
    .workshop-save-slot-delete {
      position: absolute; top: 6px; right: 6px; width: 18px; height: 18px;
      background: none; border: none; color: var(--text-muted);
      font-size: 0.7rem; line-height: 1; cursor: pointer; padding: 0;
      display: flex; align-items: center; justify-content: center;
      transition: color 0.15s; z-index: 2;
    }
    .workshop-save-slot-delete:hover { color: var(--accent-orange); }
    .workshop-save-slot-title { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: 700; font-size: 0.8rem; color: var(--workshop-blue); }
    .workshop-save-slot-title-row { display: flex; align-items: center; gap: 6px; padding-right: 16px; }
    .workshop-save-slot-rename {
      background: none; border: none; color: var(--text-muted); font-size: 0.68rem;
      cursor: pointer; padding: 0; opacity: 0; transition: opacity 0.15s, color 0.15s;
    }
    .workshop-save-slot:hover .workshop-save-slot-rename { opacity: 1; }
    .workshop-save-slot-rename:hover { color: var(--workshop-blue); }
    .workshop-save-slot-title-input {
      font-weight: 700; font-size: 0.8rem; color: var(--workshop-blue);
      font-family: inherit; border: none; border-bottom: 1px solid var(--workshop-blue);
      background: transparent; padding: 0; outline: none; flex: 1; min-width: 0;
    }
    .workshop-save-slot-snippet {
      font-size: 0.75rem; color: var(--text-dark); font-style: italic;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
    }
    .workshop-save-slot-meta { font-size: 0.62rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
    .workshop-save-slot-pct { color: var(--workshop-blue); font-weight: 700; }
    .workshop-title-divider {
      font-size: 0.62rem; color: var(--text-muted); text-transform: uppercase;
      letter-spacing: 2px; margin: 4px 0;
    }

    /* NODE MAP — a left-to-right trail of stops, pixel-badge styled,
       connected by a track that fills in as stages complete. */
    .workshop-map {
      display: flex; align-items: flex-start; gap: 0; width: 100%;
      max-width: 820px; margin: 46px 0 40px; position: relative;
    }
    .workshop-node-wrap {
      display: flex; flex-direction: column; align-items: center; gap: 8px;
      flex: 1; position: relative; z-index: 2;
    }
    /* D-PAD SHELL/NUMBER SPLIT — the flip (below) has to animate only the
       exterior shell, never the stage number sitting inside it. A single
       element can't do that (any transform on it drags its own text
       along), so the shell (.workshop-node, the thing with the
       background/border/clip-path) and the content (.workshop-node-icon,
       the number/checkmark/lock glyph) are separate, absolutely-stacked
       siblings inside this wrap rather than parent/child — the content
       simply never receives the shell's transform because it was never
       that transform's descendant in the first place. State (current/
       done/locked) lives on this shared wrap so both siblings can react
       to it independently via CSS descendant selectors instead of
       duplicating class names onto two elements from JS. */
    .workshop-node-shell-wrap {
      position: relative; width: 34px; height: 34px; cursor: pointer;
      transition: transform 0.15s;
    }
    .workshop-node-shell-wrap:hover:not(.locked) { transform: translateY(-2px); will-change: transform; }
    .workshop-node-shell-wrap.locked { cursor: not-allowed; opacity: 0.75; }
    .workshop-node {
      position: absolute; inset: 0; background: var(--paper-white);
      border: 2px solid var(--border-dark);
      transition: background-color 0.2s, border-color 0.2s;
    }
    .workshop-node-icon {
      position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
      color: var(--text-muted); pointer-events: none;
      font-family: 'Press Start 2P', 'Georgia', serif; font-size: 0.6rem;
      transition: opacity 0.2s ease;
    }
    .workshop-node-icon svg { width: 16px; height: 16px; fill: currentColor; }
    /* Finale-only: the castle's lock icon fades out in place, its own
       beat, right before the node's own re-render swaps in the unlocked
       state (a bare class-content swap has nothing to cross-fade from on
       its own — see beginEssayFinaleCastleSequence). */
    /* transition-duration override (not a change to the shared base rule
       above, which every other D-pad icon-swap also relies on) — +0.3s
       over the base 0.2s, per feedback that the lock's own fade read as
       too quick. */
    .workshop-node-icon.finale-icon-fading { opacity: 0; transition-duration: 0.5s; }
    .workshop-node-shell-wrap.current .workshop-node { background: var(--workshop-blue); border-color: var(--workshop-blue); }
    .workshop-node-shell-wrap.current .workshop-node-icon { color: #FFFFFF; }
    .workshop-node-shell-wrap.done .workshop-node { background: var(--workshop-blue-dark); border-color: var(--workshop-blue-dark); }
    .workshop-node-shell-wrap.done .workshop-node-icon { color: var(--workshop-cyan); }
    .workshop-node-shell-wrap.current.done .workshop-node { background: var(--workshop-blue); border-color: var(--workshop-blue); }
    .workshop-node-shell-wrap.current.done .workshop-node-icon { color: #FFFFFF; }
    .workshop-node-shell-wrap.locked .workshop-node { background: var(--bg-card); }
    .workshop-node-shell-wrap.locked .workshop-node-icon { color: var(--text-muted); }
    /* Flat 2D jump-and-rotate on the destination stage's D-pad shell —
       a translateY hop arcing up and back down combined with a 90°
       Z-axis spin, rather than a 3D rotateY flip. Paired with
       playWorkshopNavSound() — plays once on the freshly-rendered node
       (stage map nodes are fully rebuilt on every navigation, so there's
       no risk of restarting mid-animation on a reused element) then just
       sits in its settled state; nothing removes the class since the
       node itself gets discarded on the next render anyway. Fires for
       both direct stage-map clicks and reference-peek navigation, since
       both funnel through renderWorkshopMap's same `animateCurrentFlip`
       flag. Targets .workshop-node (the shell) specifically, never
       .workshop-node-icon — see the split above, which is exactly what
       keeps the stage number upright and fixed through the spin.
       The rotation itself never resets to 0deg — it always advances (or
       retreats, on backward navigation) by 90deg from wherever it last
       stopped, so the spin always reads as continuous forward progress
       rather than an out-and-back rubber-band. That requires a start/end
       angle that's different every single time, which a fixed set of
       @keyframes percentages can't express on its own — --flip-from-deg
       and --flip-to-deg (set inline per render from a running rotation
       counter in renderWorkshopMap, see there) carry the actual numbers
       in; @property below is what makes the browser animate the custom
       property itself smoothly rather than snapping straight to the end
       value. */
    @property --flip-from-deg { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
    @property --flip-to-deg { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
    @keyframes stage-icon-flip {
      0% { transform: translateY(0) rotateZ(var(--flip-from-deg)); }
      50% { transform: translateY(-8px) rotateZ(var(--flip-to-deg)); }
      100% { transform: translateY(0) rotateZ(var(--flip-to-deg)); }
    }
    .workshop-node-shell-wrap.current .workshop-node {
      animation: workshop-node-pulse 1.6s ease-in-out infinite;
    }
    .workshop-node-shell-wrap.stage-icon-flip .workshop-node {
      animation: workshop-node-pulse 1.6s ease-in-out infinite, stage-icon-flip 0.22s cubic-bezier(0.4, 0, 0.2, 1);
    }
    /* Finish node's castle — a full-bleed, low-contrast background
       silhouette living behind the icon layer, rather than the node's
       own outer shape being clipped into a castle (the previous
       approach). The clipped-shape version stayed "visually subtle" no
       matter how the polygon was tuned, and distorted the node out of
       the same pixel-corners silhouette every other stage uses, which
       broke the shared "progression aesthetic" of the D-pad row. As a
       watermark instead, the node's own shape stays consistent with its
       siblings, and the castle simply reads as texture behind whichever
       glyph (lock/checkmark/number) is currently showing in front of it
       — sitting between .workshop-node (the shell, z-index default) and
       .workshop-node-icon (the foreground glyph) in paint order. */
    /* will-change: filter (below) promotes this to its own compositing
       layer up front, always — not deferred until the glow's filter/
       animation first runs. Promoting it LATE (right as an animation
       starts) is a known class of browser rendering quirk that can snap
       the layer to the pixel grid a pixel or two off from where it was
       sitting un-promoted, reported as the castle visibly shifting left
       the instant the glow animation began. */
    .workshop-node-castle-bg {
      position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
      color: var(--text-muted); opacity: 0.32; pointer-events: none;
      will-change: filter;
    }
    .workshop-node-castle-bg svg { position: absolute; inset: 0; width: 100%; height: 100%; }
    /* Always teal, but clipped away to nothing outside the finale — see
       .finale-glow below for the reveal itself. Sits on top of
       castle-svg-base in paint order (later in the DOM), so once
       revealed it fully covers the gray base rather than blending with
       it. */
    .workshop-node-castle-bg .castle-svg-fill {
      color: var(--workshop-cyan); clip-path: circle(0% at 50% 100%);
    }
    .workshop-node-shell-wrap.current .workshop-node-castle-bg { color: #FFFFFF; opacity: 0.5; }
    /* Finish overrides the generic .current rule just above right back to
       a neutral gray — that rule exists for every OTHER stage node's
       icon (white reads against their solid blue .current fill), but the
       castle has no such fill behind it, so inheriting white from it
       rendered the whole silhouette as a near-invisible white ghost the
       instant the node became current (reported as "the castle turns
       completely white") — well before the finale ever touches it with
       real color. */
    .workshop-node-shell-wrap.finish-node.current .workshop-node-castle-bg { color: var(--text-muted); }
    .workshop-node-shell-wrap.done .workshop-node-castle-bg { color: var(--workshop-cyan); opacity: 0.55; }
    /* Locked (the base rule above, absent .current/.done) gets a
       dedicated flat tone here rather than inheriting --text-muted at
       low opacity — a crisp, deliberately low-contrast color reads as
       "designed to look inactive," where a faded blend reads more like
       something's rendering wrong. */
    html.dark-mode body.workshop-mode .workshop-node-shell-wrap:not(.current):not(.done) .workshop-node-castle-bg {
      color: #3A3F58; opacity: 1;
    }
    /* Finish drops the bordered/filled box every other node has and
       reads as a distinct landmark rather than another button in the
       row — the castle silhouette below is scaled up past the standard
       34px, while the lock/checkmark/number icon stays sized to its
       normal 34px slot so only the castle backdrop looks oversized,
       never its siblings. */
    /* This shell-wrap keeps the plain 34px footprint every other node
       has (no width/height override — the enlargement below is a
       transform, which never contributes to layout), so it never
       inflates .workshop-node-wrap's own flow height and never pushes
       FINISH's label down relative to its six siblings, each of which
       sits a fixed 8px (the column gap) below its own 34px box.
       overflow:visible is explicit (not just the absence of a hidden
       rule) since the castle and its flag are about to render outside
       this box on purpose — a future ancestor that clips overflow
       shouldn't be able to silently cut them off. */
    .workshop-node-shell-wrap.finish-node {
      overflow: visible;
    }
    .workshop-node-shell-wrap.finish-node .workshop-node {
      background: transparent; border: none;
    }
    /* .current.done (above, 4 classes) otherwise beats this 3-class rule
       once the castle is BOTH current and done — which it always is by
       the time it's glowing (currentStage stays 'finish' after marking
       it done) — repainting the generic blue D-pad box behind the
       castle, reported as popping into existence out of nowhere. 5
       classes here wins outright regardless of declaration order. */
    .workshop-node-shell-wrap.finish-node.current.done .workshop-node {
      background: transparent; border: none;
    }
    /* The svg itself is cropped to viewBox="0 0 24 22" with
       preserveAspectRatio="xMidYMax meet" (see the JS that builds it)
       instead of the full 0-24 square the artwork was drawn in — the
       bottom 2 units were always empty (the towers/keep stop at y=22),
       and YMax pins that now-cropped content flush against the box's
       own bottom edge. The net effect, at this element's plain inset:0
       34px box, is the castle's ground line landing exactly on this
       node's own bottom — the same baseline every standard node's
       34px box already sits on, with nothing else to compensate for. */
    /* transform-origin:bottom keeps that just-established ground line
       fixed as the scale anchor, so growing the castle only ever
       displaces it upward (and, since the origin's horizontal
       component is center, outward left/right) — never back down past
       the baseline above. scale(4/3) is derived, not eyeballed: at the
       unscaled 34px render, the roofline (the center keep's tallest
       crenellations, svg y=4 out of the cropped 22-unit height) sits
       34 - (4/22 the way up from the now content-flush bottom) *
       31.17px ≈ 8.5px above this box's own bottom edge — 25.5px short
       of reaching this node's top edge (y=0), the same ceiling the
       tops of the standard 34px boxes sit on. 34 / 25.5 = 4/3 exactly
       — the scale that closes that gap precisely, landing the roofline
       (not the flag) on the ceiling. The flag/pole above the roofline
       (svg y=0-4) scale past it in the same motion, ending up roughly
       7.6px above the ceiling — the only part of the node allowed to
       break through it. */
    .workshop-node-shell-wrap.finish-node .workshop-node-castle-bg {
      transform: scale(1.3334); transform-origin: bottom center; opacity: 0.55;
    }
    /* No .current-only opacity bump anymore (there used to be one, 0.7) —
       per feedback, there should be ZERO visual change to the castle
       between "locked" and "unlocked but not yet actually done" beyond
       the lock icon fading away; a same-instant opacity jump read as the
       castle itself getting darker. .done (below) is the one legitimate
       visual change, and even that's now secondary to the fill-reveal/
       glow animation actually doing the work of "revealing" it. */
    .workshop-node-shell-wrap.finish-node.done .workshop-node-castle-bg { opacity: 0.75; }
    /* The base layer stays gray permanently, .done included — without
       this, renderWorkshopMap marking the node .done (which it does
       synchronously, one render call before .finale-glow even gets added
       — see beginEssayFinaleCastleSequence) would let the generic
       .workshop-node-shell-wrap.done .workshop-node-castle-bg rule recolor
       THIS layer to cyan too, instantly, the moment it exists — leaving
       nothing for castle-svg-fill's own reveal to visibly grow out of.
       Only castle-svg-fill (below) is ever allowed to actually go teal,
       and only on the finale's own cue. */
    .workshop-node-shell-wrap.finish-node .castle-svg-base { color: var(--text-muted) !important; }
    /* Finale-only sequence: gray castle -> teal fill spreads outward from
       the castle's own base (matching its transform-origin) -> the
       glowing blue aura settles in once the color's actually arrived.
       Three stages instead of one instant recolor+glow, per feedback
       that the color jump itself (not the glow that follows it) was the
       jarring part. */
    @keyframes finale-castle-fill-reveal {
      from { clip-path: circle(0% at 50% 100%); }
      to { clip-path: circle(145% at 50% 100%); }
    }
    .workshop-node-shell-wrap.finish-node.finale-glow .castle-svg-fill {
      animation: finale-castle-fill-reveal 0.6s ease-out forwards;
    }
    /* drop-shadow (not box-shadow) specifically — box-shadow would glow
       the castle-bg layer's own rectangular bounding box, not the castle
       silhouette itself, since this SVG already renders past its box's
       edges (see the long comment on .workshop-node-shell-wrap.finish-
       node.workshop-node-castle-bg's scale/transform-origin above). */
    @keyframes finale-castle-glow-pulse {
      0%, 100% { filter: drop-shadow(0 0 4px var(--workshop-cyan)) drop-shadow(0 0 8px var(--workshop-cyan)); }
      50% { filter: drop-shadow(0 0 8px var(--workshop-cyan)) drop-shadow(0 0 16px var(--workshop-cyan)); }
    }
    /* Starts only once the fill-reveal above has actually finished
       (0.6s delay) — glowing before the color has even arrived would put
       the aura around a castle that's still mid-recolor. Ends exactly
       where finale-castle-glow-pulse's own 0%/100% state sits, so the
       pulse (delayed to pick up right as this fade-in's `forwards` hold
       takes over) continues with no visible jump at the handoff. */
    @keyframes finale-castle-glow-fadein {
      from { filter: drop-shadow(0 0 0px transparent) drop-shadow(0 0 0px transparent); }
      to { filter: drop-shadow(0 0 4px var(--workshop-cyan)) drop-shadow(0 0 8px var(--workshop-cyan)); }
    }
    .workshop-node-shell-wrap.finish-node.finale-glow .workshop-node-castle-bg {
      animation: finale-castle-glow-fadein 0.5s ease-out 0.6s forwards,
                 finale-castle-glow-pulse 1.4s ease-in-out infinite 1.1s;
    }
    /* State colors (current/done/locked) and the hover lift now live on
       .workshop-node-shell-wrap and its descendant selectors above — see
       the D-PAD SHELL/NUMBER SPLIT comment. Revisiting a stage you've
       already completed previously fell back to plain .done styling
       with no indication it's also the one you're currently on, which
       is why .current.done above is still declared to win over plain
       .done: still reads as completed (checkmark), but with the same
       bright, pulsing treatment .current gets on its own. */
    @keyframes workshop-node-pulse {
      0%, 100% { box-shadow: 0 0 0 0 rgba(27, 64, 216, 0.35); }
      50% { box-shadow: 0 0 0 6px rgba(27, 64, 216, 0); }
    }
    .workshop-node-label {
      font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;
      color: var(--text-muted); text-align: center; line-height: 1.2; max-width: 76px;
    }
    .workshop-node-wrap.current .workshop-node-label,
    .workshop-node-wrap.done .workshop-node-label { color: var(--text-dark); }
    /* Blocky segmented pixel track — a repeating hard-stopped gradient
       instead of a smooth fill, so both the empty track and its fill
       read as discrete pixel blocks rather than a continuous bar. */
    .workshop-track {
      position: absolute; top: 14px; height: 6px;
      background: repeating-linear-gradient(
        to right, var(--bg-card) 0px, var(--bg-card) 6px, transparent 6px, transparent 8px
      );
      z-index: 1;
      /* left/width set inline by JS after layout, spanning first-to-last
         node center — flex-based node widths aren't fixed, so this is
         measured rather than guessed in CSS. */
    }
    .workshop-track-fill {
      height: 100%; width: 0%;
      background: repeating-linear-gradient(
        to right, var(--workshop-blue) 0px, var(--workshop-blue) 6px, transparent 6px, transparent 8px
      );
      transition: width 0.5s steps(10, end);
    }
    /* The castle-reaching fill during the Stage 7 finale rides this same
       plain transition (renderWorkshopMap already drives it exactly this
       way for every ordinary stage advance) rather than a separate
       @keyframes variant — a parallel `animation`-based version here
       would fight this transition for the same `width` property (CSS
       gives `animation` priority over `transition` for a shared property,
       so the transition's own inline-style-driven width changes would
       just get silently overridden). The finale's own JS instead just
       times a matching 500ms pausable wait around this existing,
       unmodified mechanism — see beginEssayFinaleCastleSequence. */

    /* STAGE CARD */
    .workshop-stage-card {
      width: 100%; max-width: 640px; background: var(--paper-white);
      border: 2px solid var(--border-dark); box-shadow: 5px 5px 15px var(--shadow-soft);
      border-radius: 8px; padding: 28px 30px 24px; margin-bottom: 30px;
      transition: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
    }
    /* --border-dark stays bright in this palette (see the variable
       block above), so the card's own border needs redirecting to the
       actual "glowing blue-slate outline" tone here specifically,
       rather than picking up border-dark's now-bright value. */
    html.dark-mode body.workshop-mode .workshop-stage-card {
      border-color: var(--divider-muted); box-shadow: 5px 5px 0px var(--shadow-hard);
    }
    /* Transform-only — no opacity fade. An opacity ramp here used to make
       the card genuinely translucent for the first chunk of the
       animation, letting the plain page background show straight through
       it (confirmed directly: paused mid-animation via the Web Animations
       API, opacity measured ~0.5) — reported as the card looking
       "briefly transparent... allows the card behind it to be shown."
       A solid object swinging into place shouldn't fade through itself
       to do it; the transform alone (slide + rotate + settle) already
       reads as a complete entrance without ever going see-through. */
    @keyframes workshop-swing-in {
      0% { transform: translateX(70px) rotate(3.5deg); }
      55% { transform: translateX(-10px) rotate(-1.5deg); }
      75% { transform: translateX(5px) rotate(0.75deg); }
      100% { transform: translateX(0) rotate(0deg); }
    }
    .workshop-stage-card.swing-in { animation: workshop-swing-in 0.45s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
    /* Same animation, same name, same duration/easing — applied to the
       bubble column in lockstep with the card so it "flings in" alongside
       everything else instead of standing still while the card moves. */
    .workshop-side-bubbles.swing-in { animation: workshop-swing-in 0.45s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
    /* Same treatment for Stage 6's drawer — it's positioned via inline
       left/top exactly like the bubble column above (not normal document
       flow), so this transform-based animation layers on top of that
       position cleanly, the same proven way. Without this it just sat
       there dead still while the card and bubbles flew in around it. */
    .workshop-drawer.swing-in { animation: workshop-swing-in 0.45s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
    /* Positioned in JS (positionWorkshopBubbles) relative to the stage
       card's current on-screen rect, not hard-coded — the card is
       centered and its position shifts with viewport width the same way
       the main editor's page does, so a fixed pixel value would drift out
       of alignment. Overriding position:static on the buttons themselves
       since their shared classes default to position:absolute (tuned for
       the main editor's own container), which would ignore this flex
       column entirely otherwise. */
    .workshop-side-bubbles {
      position: fixed; z-index: 110;
      display: flex; flex-direction: column; gap: 12px;
      /* Smooths the JS-driven sticky-follow position updates (each
         scroll event recomputes `top` directly) into a continuous glide
         rather than a series of discrete steps — short enough to stay
         responsive, not a noticeable lag behind the actual scroll. */
      transition: top 0.08s linear;
    }
    /* Dedicated, scoped class (rather than leaning on the generic
       .btn-*-bubble names via a .workshop-side-bubbles descendant
       selector) so this dock fully owns its own layout instead of
       silently inheriting whatever position/top/right the Document
       Center's bubble palette happens to carry. That's exactly what
       broke .btn-spell-bubble here before: its base rule sets
       top:200px; right:-48px (an absolute-positioned stack offset,
       tuned for the editor's own bubble column) and switching only
       `position` to relative for the flex column — without also
       resetting top/right — left those two stale offsets in effect,
       since position:relative still honors top/right as offsets from
       its normal flow position. static ignores them entirely, which
       is why the other three siblings were never affected; explicitly
       zeroing top/right here (rather than relying on them staying
       unset) means a future change to the base bubble offsets can't
       leak into this dock again. */
    .essay-action-dock-btn {
      position: static; top: 0; right: 0;
    }
    /* Still needs to be a positioning context for its own -shell/
       -glyph children (each position:absolute against it), which
       static can't provide — relative with both offsets zeroed
       participates in the flex column identically to static. */
    .essay-action-dock-btn.btn-spell-bubble {
      position: relative;
    }
    .workshop-wrapper.title-mode .workshop-side-bubbles,
    .workshop-wrapper.title-mode #workshop-read-aloud-stack { display: none; }

    .workshop-stage-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
    .workshop-stage-label {
      font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-size: 1.35rem; font-weight: bold; color: var(--text-dark);
    }
    .workshop-info-btn {
      width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--workshop-blue);
      background: transparent; color: var(--workshop-blue); font-weight: bold; font-size: 0.72rem;
      display: flex; align-items: center; justify-content: center; cursor: pointer;
      flex-shrink: 0; transition: background-color 0.15s, color 0.15s;
    }
    .workshop-info-btn:hover, .workshop-info-btn.active { background: var(--workshop-blue); color: #FFFFFF; }
    .workshop-stage-tip {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; color: var(--text-muted);
      line-height: 1.5; margin-bottom: 14px;
    }
    /* Border and box-shadow are always physically present (2px, same
       offsets) but fully transparent at rest, rather than being added
       only via .open — a real-but-invisible transparent border still
       avoids the old "flush top+bottom border renders as a thin line
       even while collapsed" bug (transparent has no visible line to
       show), while ALSO meaning .open's color-only change is a genuine
       animatable transition instead of a class-driven snap. That snap
       was the actual bug: the border/shadow used to vanish the instant
       .open was removed, while max-height was still visibly animating
       shut for another 300ms. background-color transitions the same
       way, which is what keeps dark mode's contrasting fill (below)
       from abruptly dropping out mid-collapse too. */
    /* box-sizing:border-box (global) can only shrink the CONTENT portion
       of max-height — the border itself still occupies its full 2px+2px,
       so a "collapsed" max-height:0 panel actually renders at a real 4px
       tall box, not truly zero. That's what left a lingering thin line
       above this card: background-color used to be opaque even at rest,
       so that residual 4px of real box painted a visible bar. Background
       now follows the exact same "invisible, not absent" treatment as
       border-color/box-shadow below — transparent at rest, real color
       only once .open — so the leftover sliver of box has nothing
       opaque left to paint with. */
    .workshop-info-panel {
      max-height: 0; overflow: hidden;
      background: transparent; border-radius: 6px; margin-bottom: 0;
      border: 2px solid transparent; box-shadow: 3px 3px 0px transparent;
      transition: max-height 0.3s ease, margin 0.3s ease, border-color 0.3s ease,
        border-width 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
    }
    /* --bg-card and the page's own --bg-cream are both very dark and sit
       too close in luminance to read as a distinct box in dark mode
       specifically — the same pairing has enough contrast in light mode,
       so this is a targeted fix rather than changing the shared variable
       (which plenty of other components also rely on). */
    /* Pixel callout box — sharp corners; background/border/shadow only
       take their real values on .open (see the shared transition list
       above for how that animates in cleanly instead of snapping). */
    .workshop-info-panel.open {
      max-height: 420px; margin-bottom: 16px; overflow-y: auto;
      background: var(--bg-card);
      border-radius: 0; border-color: #000000; box-shadow: 3px 3px 0px #000000;
    }
    /* Background no longer needs its own override here — it's just
       var(--bg-card) from the base rule above, and that variable's
       own workshop-dark value (#222436) already gives the exact tone
       the arcade palette wants; only the border treatment (a subtle
       outline the rest of the way around, plus a bold blue accent
       specifically on the left) needs setting explicitly. */
    html.dark-mode body.workshop-mode .workshop-info-panel.open {
      border-color: var(--divider-muted);
      border-left: 4px solid var(--workshop-blue);
      box-shadow: 3px 3px 0px var(--shadow-hard);
    }
    .workshop-info-panel-inner { padding: 16px 18px; font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.82rem; line-height: 1.6; color: var(--text-dark); }
    .workshop-info-panel-inner::before {
      content: '[ HINT ]'; display: inline-block; font-family: 'Press Start 2P', monospace;
      font-size: 0.5rem; background: #000000; color: #FFFFFF; padding: 4px 6px; margin-bottom: 10px;
    }
    .workshop-info-panel-inner p { margin: 0 0 10px; }
    .workshop-info-example {
      display: block; margin: 6px 0 12px; padding-left: 12px;
      border-left: 3px solid var(--workshop-blue); color: var(--text-dark);
      font-style: italic;
    }
    .workshop-textarea {
      width: 100%; min-height: 110px; resize: none;
      font-family: 'Georgia', serif; font-size: 1rem; line-height: 1.5;
      border: 2px solid #D8D4CC; border-radius: 6px; padding: 14px;
      background: var(--bg-cream); color: var(--text-dark);
      transition: border-color 0.15s, background-color 0.4s ease; box-sizing: border-box; display: block;
    }
    .workshop-textarea:focus { outline: none; border-color: var(--workshop-blue); }
    /* Same #D8D4CC-never-adapts issue as .evidence-type-btn above, plus
       background: var(--bg-cream) would otherwise match the page
       behind it exactly (bg-cream IS the workshop's own page background
       in this palette) — recessed instead of blending in is the whole
       point of "depth inside the main canvas," so it gets its own
       dedicated tone, darker than any of the three shared surface
       tiers rather than reusing one of them for a role none quite fit. */
    html.dark-mode body.workshop-mode .workshop-textarea { background: #161724; border-color: var(--divider-muted); }
    html.dark-mode body.workshop-mode .workshop-textarea:focus { border-color: var(--workshop-blue); }
    /* Native resize handles can't be restyled (they're browser chrome, not
       a stylable element), so a custom one replaces it entirely — which
       also means height can be clamped in JS to never go below the box's
       own opening size, something native resize can't selectively enforce
       per-instance either. */
    .workshop-textarea-wrap { position: relative; }
    .workshop-resize-handle {
      position: absolute; bottom: 6px; right: 8px; width: 16px; height: 16px;
      cursor: ns-resize; color: var(--text-muted); opacity: 0.55;
      display: flex; align-items: center; justify-content: center;
      transition: opacity 0.15s, color 0.15s;
    }
    .workshop-resize-handle:hover { opacity: 1; color: var(--workshop-blue); }
    .workshop-stage-footer {
      display: flex; align-items: center; justify-content: space-between; margin-top: 16px;
    }
    /* translateY nudge per Ethan — DONE!'s own text (Press Start 2P,
       padding: 12px 24px) doesn't sit at the same optical middle as this
       plain sans-serif label despite flex align-items:center putting
       their BOXES on the same line; this is every stage's footer (one
       shared class), so the fix lands everywhere at once. */
    .workshop-word-count { font-size: 0.68rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; transform: translateY(2px); }
    .workshop-done-btn {
      background: #3FA34D; color: #FFFFFF; border: 2px solid var(--border-dark);
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 12px;
      padding: 12px 24px; cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      opacity: 0.35; pointer-events: none; transition: opacity 0.3s ease, transform 0.1s ease;
      text-transform: uppercase; letter-spacing: 1px;
      display: flex; align-items: center; justify-content: center;
    }
    .workshop-done-btn.ready { opacity: 1; pointer-events: auto; }
    .workshop-done-btn.ready:hover { transform: translateY(-2px); }
    .workshop-done-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(--border-dark); }

    .workshop-placeholder {
      text-align: center; padding: 30px 10px; color: var(--text-muted);
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.92rem; line-height: 1.6;
    }

    /* Numbered-box list pattern — Topic Sentences today, Evidence and
       Reasoning will reuse this same shape later since they're the same
       "one box per topic, add/remove freely" idea. */
    .workshop-item-box {
      position: relative; margin-bottom: 14px; padding: 14px 34px 12px 16px;
      border: 2px solid #D8D4CC; border-radius: 6px; background: var(--bg-cream);
    }
    .workshop-item-label {
      font-weight: 700; font-size: 0.72rem; color: var(--text-muted);
      text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;
    }
    .workshop-item-box .workshop-textarea { min-height: 54px; }
    .workshop-item-remove {
      position: absolute; top: 10px; right: 10px; width: 18px; height: 18px;
      background: none; border: none; color: var(--text-muted);
      font-size: 0.7rem; line-height: 1; cursor: pointer; padding: 0;
      display: flex; align-items: center; justify-content: center;
      transition: color 0.15s;
    }
    .workshop-item-remove:hover { color: var(--accent-orange); }
    /* 16-bit-styled "add" control — a solid blue pixel-bordered box
       holding the "+" glyph, paired with a plain bold text label, rather
       than the previous dashed-outline placeholder-style button. Matches
       the same hard-shadow pixel-button language used elsewhere in
       Build-an-Essay (citation mode buttons, action bubbles). */
    .workshop-add-item-btn {
      display: flex; align-items: center; gap: 10px; width: 100%; margin-bottom: 18px;
      padding: 8px 10px; background: transparent; border: none;
      cursor: pointer; transition: opacity 0.15s;
    }
    .workshop-add-item-btn:hover { opacity: 0.8; }
    .workshop-add-item-btn:disabled { opacity: 0.4; cursor: default; }
    .workshop-add-item-icon {
      flex-shrink: 0; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
      background: var(--workshop-blue); border: 2px solid #000000; box-shadow: 2px 2px 0px #000000;
      color: #FFFFFF; font-family: 'Press Start 2P', monospace; font-size: 0.9rem; line-height: 1;
    }
    .workshop-add-item-label {
      color: var(--workshop-blue); font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      font-weight: 700; font-size: 0.8rem; text-align: left;
    }

    /* STAGE 5: YOUR ESSAY SO FAR — reuses .workshop-item-box AND
       .workshop-item-label directly (same class, not a lookalike copy),
       so the "PARAGRAPH 1" headers are pixel-identical in font, size,
       and above-the-box left-aligned placement to "TOPIC SENTENCE 1" on
       the earlier stage — no dedicated label class needed here at all. */
    /* The 16-bit connecting track between paragraph boxes — same dashed-
       gradient language as the top D-pad progress track (.workshop-track/
       .workshop-track-fill), just rotated to vertical and driven by
       focus progress through the list instead of stage completion. See
       buildDraftStage's fillConnector/handleParagraphFocus for the
       instant-vs-animated fill logic (skipped-over gaps snap instantly,
       only the newly-entered gap actually animates). */
    .draft-connector { position: relative; width: 6px; height: 26px; margin: 0 auto 14px; }
    .draft-connector-track {
      position: absolute; inset: 0;
      background: repeating-linear-gradient(to bottom, var(--bg-card) 0px, var(--bg-card) 6px, transparent 6px, transparent 8px);
    }
    .draft-connector-fill {
      position: absolute; top: 0; left: 0; right: 0; height: 0%;
      background: repeating-linear-gradient(to bottom, var(--workshop-blue) 0px, var(--workshop-blue) 6px, transparent 6px, transparent 8px);
      transition: height 0.5s steps(6, end);
    }

    /* EVIDENCE STAGE */
    .evidence-card {
      position: relative; margin-bottom: 18px; padding: 16px 18px 14px;
      border: 2px solid #D8D4CC; border-radius: 8px; background: var(--bg-cream);
    }
    .evidence-card.just-cited {
      animation: evidence-card-glow 1.6s ease;
    }
    @keyframes evidence-card-glow {
      0% { box-shadow: 0 0 0 rgba(27, 64, 216, 0); border-color: #D8D4CC; }
      25% { box-shadow: 0 0 22px rgba(27, 64, 216, 0.55); border-color: var(--workshop-blue); }
      100% { box-shadow: 0 0 0 rgba(27, 64, 216, 0); border-color: #D8D4CC; }
    }
    .evidence-topic-context {
      background: var(--bg-card); border: 2px solid #000000; border-radius: 0;
      box-shadow: 3px 3px 0px #000000; padding: 10px 14px; margin-bottom: 20px;
    }
    .evidence-topic-context .context-label {
      display: block; font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase;
      letter-spacing: 0.5px; font-weight: 700; margin-bottom: 4px;
    }
    .evidence-topic-context span.topic-snippet {
      font-weight: 400; font-style: italic;
      color: var(--text-dark); font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; line-height: 1.5;
    }
    /* Quote / Paraphrase — a plain two-pill toggle, not a full tab bar
       (this is a minor mode switch, not a navigation choice, so it
       shouldn't visually compete with the CITE flow's own TYPE/PASTE/
       UPLOAD selector later). */
    .evidence-type-toggle { display: flex; gap: 8px; margin-bottom: 8px; }
    .evidence-type-btn {
      background: var(--paper-white); border: 2px solid #D8D4CC; border-radius: 20px;
      padding: 5px 14px; font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      font-weight: 700; font-size: 0.72rem;
      color: var(--text-muted); cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px;
      transition: all 0.15s ease;
      /* Container stays plain/symmetric — no unequal padding or margin
         here to fake the optical correction; that lives entirely on the
         inner label span below, via transform, so it can't drift the
         pill's own hover/active bounds. */
      display: inline-flex; align-items: center; justify-content: center;
    }
    .evidence-type-btn.active { background: var(--workshop-blue); border-color: var(--workshop-blue); color: #FFFFFF; }
    /* The inactive pill's #D8D4CC border was hardcoded, never adapting
       to dark mode at all — a light warm-grey that would look out of
       place against the arcade palette. Overridden here rather than
       changed at the source so light mode (and the Document Center's
       own dark mode, which never sees this workshop-only component
       anyway) stay exactly as they were. */
    html.dark-mode body.workshop-mode .evidence-type-btn { border-color: var(--divider-muted); }
    /* Avant Garde Demi's cap-height sits optically high-and-right of a
       flex-centered box — .evidence-type-btn's own text-transform:
       uppercase makes this especially visible on all-caps QUOTE/
       PARAPHRASE. Nudging just this inner span (not the button, whose
       own box is what hover/active paints against) corrects the drift
       without touching the pill's layout. */
    .evidence-type-btn-label {
      display: inline-block; transform: translateY(1.75px) translateX(-1px);
    }
    .evidence-quote-wrap { display: flex; flex-direction: column; }
    /* Real dedicated rows, not absolutely-positioned overlap — previously
       the marks sat on top of the textarea at low opacity, and the
       closing one got clipped/hard to see entirely. Framing them in
       their own space above/below solves both: full color, nothing
       overlapping, room to actually be a visual moment rather than a
       faint corner decoration. */
    .evidence-quote-mark-row {
      display: flex; height: 50px; overflow: visible;
    }
    .evidence-quote-mark-row.open { justify-content: flex-start; align-items: flex-start; }
    .evidence-quote-mark-row.close { justify-content: flex-end; align-items: flex-start; margin-top: 5px; }
    /* Pixelify Sans, regular weight — reuses the same retro-game font
       already established for the workshop title logo elsewhere, rather
       than a hand-drawn shape. Real quotation mark glyphs read as
       exactly that, not an abstract icon that could be mistaken for
       something else entirely. */
    .evidence-quote-pixel-font {
      font-family: 'Pixelify Sans', 'Georgia', serif; font-weight: 400;
      font-size: 6.5rem; line-height: 0.5; color: var(--workshop-blue);
    }
    /* Quote-mark glyphs sit near cap-height (the top of their line box),
       not near the baseline — confirmed directly via pixel analysis:
       the visible ink sits flush against the row's TOP regardless of
       align-items, since the line-height-squashed content overflows the
       row and ignores flex alignment once it's taller than the row
       itself. The close mark (naturally already adjacent to the note
       above it) needs no correction; the open mark (which needs to hug
       the textarea BELOW it instead) needs an explicit downward shift
       to actually reach it.
       The shift below was originally derived by measurement, not
       eyeballing: using this font's real Canvas TextMetrics
       (actualBoundingBoxAscent/Descent) to find where the glyph's
       actual ink sits within its line box, the gap from the open
       mark's ink to the textarea's top edge was ~14.3px while the gap
       from the textarea's bottom edge to the close mark's ink was
       ~20.2px. Closing that ~5.9px difference meant shifting the open
       mark UP (a *smaller* translateY, since this pushes it away from
       the textarea) from 22px to 16.1px, which measured out to an
       exact match (20.16px both sides). Per Ethan, nudged back down 2px
       total from that exact match (16.1px -> 18.1px) — a deliberate,
       intentional offset off of true-equal, not a measurement error to
       "fix" later. */
    .evidence-quote-pixel-font.open { transform: translateY(18.1px); }
    .evidence-quote-wrap .workshop-textarea { padding-left: 14px; padding-right: 14px; }
    .evidence-auto-note {
      font-size: 0.72rem; color: var(--text-muted); font-style: italic;
      margin-top: 4px; font-family: 'ITC Avant Garde Gothic Book', sans-serif;
    }
    .evidence-intext-row {
      display: flex; align-items: center; gap: 8px; margin-top: 8px;
    }
    .evidence-intext-row label {
      font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase;
      letter-spacing: 0.5px; font-weight: 700; white-space: nowrap;
    }
    .evidence-intext-input {
      flex: 1; border: 2px solid #D8D4CC; border-radius: 5px; padding: 6px 10px;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.85rem; background: var(--paper-white);
      color: var(--text-dark); transition: border-color 0.15s;
    }
    .evidence-intext-input:focus { outline: none; border-color: var(--workshop-blue); }
    .evidence-skip-link {
      background: none; border: none; color: var(--text-muted); font-size: 0.72rem;
      text-decoration: underline; cursor: pointer; padding: 0; margin-top: 10px;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif;
    }
    .evidence-skip-link:hover { color: var(--accent-orange); }
    .evidence-skipped-note {
      font-size: 0.8rem; color: var(--text-muted); font-style: italic; padding: 8px 0;
      display: flex; align-items: center; justify-content: space-between; font-family: 'ITC Avant Garde Gothic Book', sans-serif;
    }
    .evidence-unskip-link {
      background: none; border: none; color: var(--workshop-blue); font-size: 0.72rem;
      text-decoration: underline; cursor: pointer; font-family: 'ITC Avant Garde Gothic Book', sans-serif;
    }
    .evidence-card-footer {
      display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 14px;
    }
    .evidence-cite-btn {
      position: relative; background: var(--paper-white); border: 2px solid var(--border-dark);
      border-radius: 5px; padding: 8px 16px 8px 14px; font-family: 'Press Start 2P', 'Georgia', serif;
      font-weight: normal; font-size: 10px; cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      text-transform: uppercase; letter-spacing: 0.5px; transition: transform 0.1s ease;
      display: flex; align-items: center; gap: 6px;
    }
    .evidence-cite-btn:hover { transform: translateY(-1px); }
    .evidence-cite-btn:active { transform: translateY(1px); box-shadow: 0px 0px 0px var(--border-dark); }
    .evidence-cite-btn.cited { background: var(--workshop-blue); border-color: var(--workshop-blue); color: #FFFFFF; }
    .evidence-cite-arrow { display: flex; align-items: center; }
    .evidence-cite-arrow svg { width: 13px; height: 13px; }
    /* Pixel exclamation point (see the inline SVG for the actual glyph),
       replacing the old plain "*" text glyph — floats continuously like
       a 2D-platformer pickup, deliberately more attention-grabbing than
       the check's own aura below, since this one genuinely needs the
       student to notice and act. */
    .evidence-cite-alert {
      color: #D42E2E; display: flex; align-items: center; cursor: help;
      animation: evidence-cite-alert-float 1s ease-in-out infinite;
      /* Nudged left (via the gap this opens up before the CITE button,
         not a transform — that's already spoken for by the float
         keyframe above) to restore the breathing room the size bump
         below ate into. */
      margin-right: 4px;
    }
    .evidence-cite-alert svg { width: 17px; height: 17px; }
    @keyframes evidence-cite-alert-float {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-3px); }
    }
    /* Pixel checkmark, steady — no scale/fade of its own anymore. The
       "still noticeable but quiet" confirmation cue now lives entirely
       in the ::after aura — a faint filled circle expanding outward from
       the glyph and fading, radar-ping style, on a continuous ~1.7s
       loop. position:relative here is just so that ::after's inset
       anchors to the glyph itself, not some ancestor. */
    .evidence-cite-check {
      position: relative; color: #3FA34D; display: flex; align-items: center; cursor: help;
      margin-right: 4px;
    }
    .evidence-cite-check svg { width: 17px; height: 17px; }
    .evidence-cite-check::after {
      content: ''; position: absolute; inset: -5px; border-radius: 50%;
      background: #3FA34D; opacity: 0; pointer-events: none;
      animation: evidence-check-aura 2s ease-out infinite;
    }
    @keyframes evidence-check-aura {
      0% { transform: scale(0.5); opacity: 0.25; }
      100% { transform: scale(2); opacity: 0; }
    }

    /* REASONING STAGE */
    .reasoning-card {
      position: relative; margin-bottom: 18px; padding: 16px 18px 14px;
      border: 2px solid #D8D4CC; border-radius: 8px; background: var(--bg-cream);
    }
    .reasoning-refs { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
    /* Inventory-slot styling — hard black frame + offset drop shadow,
       same "arcade button" language as .workshop-info-panel.open and
       the 8-bit action bubbles elsewhere in the workshop, rather than
       the soft rounded card this used to be. The color-coded accent bar
       (blue/orange, matching every other Topic Sentence/Evidence cue in
       the app) moves off the border itself and onto a dedicated ::before
       strip inset just past the frame — "an accent block along the left
       INNER edge," not the frame's own edge — which is also what frees
       the border up to react to hover/press on its own without fighting
       over the same property. */
    .reasoning-ref-btn {
      position: relative;
      display: block; text-align: left; width: 100%;
      background: var(--bg-card); border: 2px solid #000000; border-radius: 0;
      padding: 10px 14px 10px 22px; cursor: pointer;
      box-shadow: 2px 2px 0px #000000;
      transition: transform 0.1s ease, box-shadow 0.1s ease, border-color 0.1s ease;
      font-family: inherit;
    }
    .reasoning-ref-btn::before {
      content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
      background: var(--workshop-blue);
    }
    .reasoning-ref-btn.evidence-ref::before { background: var(--accent-orange); }
    /* Hover: a small lift plus a growing shadow reads as the card
       floating slightly off the surface, primed to be pressed — the
       border shifting to the same accent color as the card's own type
       (blue/orange) is the "light border shift," a second cue beyond
       just the cursor that this specific card is the one about to
       react. Active: the exact inverse — dips 2px down and the shadow
       goes flat (0px), the classic hard-shadow "pressed into the
       surface" read, the same physics-of-a-button language the rest of
       this arcade styling already uses elsewhere. The pre-existing
       navigation + its stage-change SFX (navigateToTopicCard /
       playWorkshopNavSound) live entirely in JS and aren't touched by
       any of this — purely a visual restyle of the same button. */
    .reasoning-ref-btn:hover {
      transform: translateY(-1px);
      box-shadow: 4px 4px 0px #000000;
      border-color: var(--workshop-blue);
    }
    .reasoning-ref-btn.evidence-ref:hover { border-color: var(--accent-orange); }
    .reasoning-ref-btn:active {
      transform: translateY(2px);
      box-shadow: 0px 0px 0px #000000;
    }
    /* Same small solid dark tag treatment as the Hint panel's own
       "[ HINT ]" label (.workshop-info-panel-inner::before) — Press
       Start 2P turns the category name into a crisp pixel badge instead
       of plain muted uppercase text. */
    .reasoning-ref-label {
      display: inline-block;
      font-family: 'Press Start 2P', monospace; font-size: 0.5rem;
      text-transform: uppercase; line-height: 1;
      background: #000000; color: #FFFFFF;
      padding: 4px 6px; margin-bottom: 8px;
    }
    .reasoning-ref-text {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-style: italic; font-size: 0.83rem;
      color: var(--text-dark); line-height: 1.4;
    }
    .reasoning-card-footer { margin-top: 12px; }

    /* CITATION PAGE — its own full view within the Evidence stage,
       swapped in when CITE is pressed. Slides in from the right like a
       new "level," slides back out to the left on Back — same idea as
       every other workshop transition, just horizontal instead of the
       vertical swing-in the stage cards use. */
    .citation-page { animation: citation-slide-in 0.28s ease; }
    @keyframes citation-slide-in {
      from { opacity: 0; transform: translateX(24px); }
      to { opacity: 1; transform: translateX(0); }
    }
    .citation-page-header {
      display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;
    }
    .citation-back-btn {
      background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 50%;
      width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
      cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark); transition: transform 0.1s ease;
      color: var(--text-dark);
    }
    .citation-back-btn:hover { transform: translateX(-2px); }
    .citation-back-btn:active { transform: translateY(1px); box-shadow: 0px 0px 0px var(--border-dark); }
    .citation-back-btn svg { width: 16px; height: 16px; }
    .citation-page-title { font-family: 'ITC Souvenir Medium', 'Georgia', serif; font-weight: bold; font-size: 1.35rem; color: var(--text-dark); }
    .citation-snippet {
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-style: italic; font-size: 0.85rem; color: var(--text-muted);
      background: var(--bg-card); border: 2px solid #000000; border-radius: 0;
      box-shadow: 3px 3px 0px #000000; padding: 10px 14px; margin-bottom: 22px; line-height: 1.5;
    }
    /* TYPE IT / PASTE IT / UPLOAD IT — three chunky "pick your approach"
       buttons, video-game-menu styled to match SET GOAL / DONE! / the
       stage map nodes rather than a plain form control. */
    .citation-mode-row { display: flex; gap: 10px; margin-bottom: 18px; }
    .citation-mode-btn {
      flex: 1; background: var(--paper-white); border: 2px solid var(--border-dark); border-radius: 8px;
      padding: 14px 8px; font-family: 'ITC Avant Garde Gothic Demi', sans-serif;
      font-weight: 800; font-size: 0.82rem;
      cursor: pointer; box-shadow: 3px 3px 0px var(--border-dark); text-transform: uppercase;
      letter-spacing: 0.5px; color: var(--text-dark); transition: transform 0.1s ease, background-color 0.15s ease, color 0.15s ease;
      display: flex; flex-direction: column; align-items: center; gap: 6px;
    }
    .citation-mode-btn svg { width: 22px; height: 22px; }
    .citation-mode-btn:hover { transform: translateY(-2px); }
    .citation-mode-btn.active {
      background: var(--workshop-blue); border-color: var(--workshop-blue); color: #FFFFFF;
      transform: translateY(1px); box-shadow: 1px 1px 0px var(--border-dark);
    }
    .citation-panel { animation: citation-panel-pop 0.18s ease; }
    @keyframes citation-panel-pop {
      from { opacity: 0; transform: scale(0.97); }
      to { opacity: 1; transform: scale(1); }
    }
    .citation-field-row { margin-bottom: 12px; }
    .citation-field-label {
      font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase;
      letter-spacing: 0.5px; font-weight: 700; margin-bottom: 4px; display: block;
    }
    .citation-field-input {
      width: 100%; border: 2px solid #D8D4CC; border-radius: 5px; padding: 8px 10px;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.88rem; background: var(--paper-white);
      color: var(--text-dark); transition: border-color 0.15s; box-sizing: border-box;
    }
    .citation-field-input:focus { outline: none; border-color: var(--workshop-blue); }
    .citation-paste-textarea {
      width: 100%; min-height: 90px; border: 2px solid #D8D4CC; border-radius: 6px; padding: 12px;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; font-size: 0.9rem; line-height: 1.5; resize: none;
      background: var(--paper-white); color: var(--text-dark); box-sizing: border-box;
      transition: border-color 0.15s;
    }
    .citation-paste-textarea:focus { outline: none; border-color: var(--workshop-blue); }
    .citation-parse-result {
      margin-top: 10px; padding: 10px 12px; border-radius: 6px; font-size: 0.78rem;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; line-height: 1.5;
    }
    .citation-parse-result.good { background: #E8F5EA; color: #2A6B34; }
    .citation-parse-result.partial { background: #FFF3DC; color: #8A6416; }
    .citation-upload-zone {
      border: 2px dashed #C9C4B8; border-radius: 8px; padding: 26px 16px; text-align: center;
      cursor: pointer; transition: border-color 0.15s, background-color 0.15s; color: var(--text-muted);
    }
    .citation-upload-zone:hover, .citation-upload-zone.drag-over {
      border-color: var(--workshop-blue); background: var(--bg-card); color: var(--workshop-blue);
    }
    .citation-upload-zone svg { width: 28px; height: 28px; margin-bottom: 6px; }
    .citation-upload-zone .upload-label { font-family: 'ITC Avant Garde Gothic Demi', sans-serif; font-weight: 700; font-size: 0.85rem; }
    .citation-upload-zone .upload-sub { font-size: 0.7rem; margin-top: 3px; }
    .citation-page-footer { display: flex; justify-content: flex-end; margin-top: 20px; }
    .citation-works-cited-note {
      font-size: 0.72rem; color: var(--text-muted); font-style: italic;
      font-family: 'ITC Avant Garde Gothic Book', sans-serif; margin-top: 6px;
    }
    .citation-done-btn {
      background: #3FA34D; color: #FFFFFF; border: 2px solid var(--border-dark);
      font-family: 'Press Start 2P', 'Georgia', serif; font-weight: normal; font-size: 12px;
      padding: 12px 24px; cursor: pointer; box-shadow: 2px 2px 0px var(--border-dark);
      text-transform: uppercase; letter-spacing: 1px; transition: transform 0.1s ease;
      display: flex; align-items: center; justify-content: center;
    }
    .citation-done-btn:hover { transform: translateY(-2px); }
    .citation-done-btn:active { transform: translateY(2px); box-shadow: 0px 0px 0px var(--border-dark); }

    .workshop-word-total-wrapper {
      position: fixed; bottom: 16px; right: 24px; z-index: 120;
      display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
    }
    .workshop-word-total {
      font-size: 0.78rem; text-transform: uppercase; letter-spacing: 1px;
      color: var(--text-muted); font-weight: 600; text-align: right;
    }
    .workshop-word-total strong { color: var(--workshop-blue); font-weight: 700; font-size: 1rem; }
    /* Same idea as the Document Center's own #page-estimate-display
       (smaller, muted, sitting right under the word count) — "in MLA"
       on the end since 250-words-per-page is specifically an MLA
       (double-spaced, 12pt) convention, not a universal one, so the
       number means nothing without naming the metric it's estimating
       against.
       0.8rem, not an arbitrary "a bit bigger" — measured directly
       against .workshop-word-total at a real word count (692 words /
       2.8 pages, matching what Ethan was actually looking at): both
       lines are right-aligned off the same edge already, so matching
       this line's rendered WIDTH to that one's is what makes their LEFT
       edges land flush too, reading as one straight column on both
       sides instead of just the right. 0.62rem measured ~107px against
       the word line's ~138px there; scaling by that same ratio lands
       almost exactly on 0.8rem. */
    .workshop-page-estimate {
      font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px;
      color: var(--text-muted); font-weight: 600; text-align: right; line-height: 1.3;
    }
    .workshop-page-estimate strong { color: var(--workshop-blue); font-weight: 700; }

    /* STAGE 6 DRAWER — a stacked card tucked underneath the main stage
       card (see the HTML comment for the overall architecture). Its own
       width is CONSTANT the whole time; only left/top (set by
       positionWorkshopDrawer, in JS) change between the peeking and
       fully-open states, animated by this transition — the "physical
       drawer sliding out" illusion is entirely a position change, not a
       width/clip change, which is also exactly what makes the
       overflow-x:auto trick on .workshop-wrapper work (a clipped/resized
       box wouldn't correctly contribute to that element's own scrollable
       bounds the way a genuinely-positioned one does). Same card look
       (background/border/shadow/radius) as .workshop-stage-card, for the
       same "these are physically the same kind of object" reason a real
       stacked-cards illusion depends on. */
    .workshop-drawer {
      position: absolute; width: 504px; z-index: 1;
      background: var(--paper-white); border: 2px solid var(--border-dark);
      border-radius: 8px; box-shadow: 5px 5px 15px var(--shadow-soft);
      padding: 44px 20px 16px;
      transition: left 0.4s cubic-bezier(0.16, 1, 0.3, 1), top 0.4s cubic-bezier(0.16, 1, 0.3, 1);
    }
    /* Explicit stacking context + z-index above the drawer's own 1 — the
       card needs to be a POSITIONED element for z-index to do anything at
       all (it wasn't one before), and needs to reliably paint OVER the
       drawer's peeking sliver regardless of DOM order or ambient flex/
       stacking quirks, since the peeking illusion depends on the card
       visually covering all but that sliver. */
    .workshop-stage-card { position: relative; z-index: 2; }
    /* Bare glyph, no button chrome (no circle/border/fill) — hover
       feedback is carried entirely by the slight enlarge below, on the
       BUTTON wrapper rather than the svg itself, specifically so it
       lives on a separate `transform` than the svg's own flip and the
       two never have to share one transition duration (the flip needs
       its own deliberate 0.4s; a hover response needs to feel snappy). */
    .workshop-drawer-toggle {
      position: absolute; top: 12px; left: 12px; width: 28px; height: 28px;
      background: none; border: none; color: var(--border-dark);
      display: flex; align-items: center; justify-content: center;
      cursor: pointer; padding: 0; transition: transform 0.15s ease, color 0.15s ease;
    }
    .workshop-drawer-toggle:hover { transform: scale(1.15); color: var(--workshop-blue); }
    .workshop-drawer-toggle svg { width: 15px; height: 15px; }
    /* The arrow itself flips left-to-right in step with the slide instead
       of swapping icons abruptly — same transition timing as the drawer's
       own position change above, so the two read as one continuous
       motion rather than a snap partway through it. Untouched by the
       hover-enlarge above since that lives on the button, not this svg. */
    .workshop-drawer-toggle svg { transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
    .workshop-drawer.open .workshop-drawer-toggle svg { transform: scaleX(-1); }
    .workshop-drawer-title {
      font-weight: 700; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.5px;
      color: var(--text-dark);
    }
    .workshop-drawer-readonly-label {
      font-weight: 600; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.5px;
      color: var(--text-muted); margin-top: 2px;
    }
    .workshop-drawer-body { margin-top: 14px; }
    /* Muted paper + dimmer text (reusing --bg-card, the workshop's own
       existing "nested/inner tier" surface — same variable the hint
       panel and locked D-pad nodes already use) instead of a fresh color,
       signaling "locked reference material" through the same visual
       language the app already uses for that idea elsewhere, rather than
       inventing a new one. */
    .workshop-drawer-textarea { background: var(--bg-card); color: var(--text-muted); cursor: default; }
    .workshop-drawer-textarea:focus { border-color: #D8D4CC; }

    /* PIXELATED SPEECH-BUBBLE TOAST — replaces the plain black status pill
       for Build-an-Essay completions specifically (the shared #toast is
       untouched, still used everywhere else — "Saved", autosave, etc).
       Sits to the LEFT of the current stage card with a comfortable
       overlap into its left edge, vertically centered on that card — and
       specifically the card for the stage just arrived at, not the one
       just finished, since basing position on a card that's already gone
       would be pointless. Two nested elements because a single element
       can only have one clip-path: the outer one holds the actual pixel-
       corner speech-bubble SHAPE (reusing the .pixel-corners technique
       already used elsewhere), the inner one holds the left-to-right
       reveal-by-steps ANIMATION. */
    .workshop-toast-bubble {
      position: fixed; width: 220px; z-index: 390; pointer-events: none;
      background: var(--paper-white); border: 3px solid var(--border-dark);
      box-shadow: 4px 4px 0px var(--border-dark);
      opacity: 0; transition: opacity 0.15s ease;
    }
    .workshop-toast-bubble.show { opacity: 1; }
    .workshop-toast-reveal {
      padding: 9px 11px;
      clip-path: inset(0 100% 0 0);
    }
    @keyframes workshop-toast-pixel-reveal { to { clip-path: inset(0 0% 0 0); } }
    .workshop-toast-bubble.revealing .workshop-toast-reveal {
      animation: workshop-toast-pixel-reveal 0.32s steps(8, end) forwards;
    }
    .workshop-toast-text {
      display: block; font-family: 'Pixelify Sans', 'Georgia', serif;
      font-size: 1.15rem; line-height: 1.55; color: var(--text-dark); text-align: center;
    }
    .workshop-toast-bold { font-weight: 600; }

    /* Bare-minimum "celebration" for every stage completion — a handful of
       yellow sparkles thrown outward and gone within a second. Confetti is
       saved for the true final completion; this is the lightweight,
       every-time version. */
    @keyframes workshop-sparkle-pop {
      0% { transform: scale(0) rotate(0deg); opacity: 0; }
      35% { transform: scale(1.15) rotate(15deg); opacity: 1; }
      /* One extra flicker — a brief dip and return — before the real
         fade-out, rather than a single clean pop-then-fade. */
      50% { transform: scale(0.9) rotate(20deg); opacity: 0.35; }
      65% { transform: scale(1.05) rotate(28deg); opacity: 1; }
      100% { transform: scale(0.4) rotate(40deg) translateY(-26px); opacity: 0; }
    }
    .workshop-sparkle {
      position: fixed; pointer-events: none; z-index: 500;
      color: #F2C94C; text-shadow: 0 0 4px rgba(242, 201, 76, 0.6);
      animation: workshop-sparkle-pop 1.4s ease-out forwards;
    }
