@import url('https://fonts.googleapis.com/css2?family=VT323&family=Inter:wght@400;700&family=Chakra+Petch:wght@400;600;700&display=swap');

/* Terminal/BBS redesign (2026-07-08), replacing the earlier arcade-cabinet-
   bezel theme. Confirmed direction: a genuine command-line system, not a
   video-game skin — VT323 only (no Press Start 2P, no JetBrains Mono),
   hierarchy carried by size/case/letter-spacing/glow instead of a second
   typeface, box-drawing-style double-line frames (native CSS border
   mitering, not clip-path or font-dependent glyphs), bracket-style reverse-
   video buttons, and real CRT phosphor glow via layered text-shadow. See
   DESIGN.md for the full rationale and named rules.

   Inter is the one deliberate, scoped exception (2026-07-13): the Staff
   Manual page's body paragraphs/lists use it instead of VT323, chosen from
   a mockup the user picked (see MEMORY.md) — long-form reading text in a
   pixel font was hard to read for the manual's non-technical, non-gamer
   pilot audience. Headings, the sidebar, and every other page keep VT323
   unchanged; this is the second such exception after the manual's own
   print stylesheet further down this file. */
:root {
  --ql-bg: #04120b;
  --ql-panel: #071f14;
  --ql-accent: #39ff8a;
  --ql-accent-cyan: #36ffff;
  --ql-text: #d8f5e8;
  --ql-muted: #6b9c86;
  /* Scoped to the Delete My Data danger zone only (Account page) — a
     deliberate, direct-request exception to this product's otherwise
     alarm-free palette (see DESIGN.md), reserved for exactly one
     irreversible action. Never used for stats or anything gain-related. */
  --ql-danger: #ff3b3b;
  /* Scoped to Food Badge icons only (DESIGN.md "Food Badge Icon Colors") —
     never used as a border, text, or button color anywhere else. */
  --ql-food-fruit: #ff5a5a;
  --ql-food-protein: #ff9b52;
  --ql-food-grain: #e0c56b;
}

* {
  box-sizing: border-box;
}

/* General-purpose hide utility. Don't delete this thinking it's redundant
   with .ql-modal-overlay.ql-hidden or .ql-breakdown-item__detail.ql-hidden
   further down — those compound selectors exist *in addition to* this
   one, deliberately, so they out-specificity that element's own
   `display` value regardless of source order (see the comment at each
   rule). Deleting this generic rule instead of adding the compound one
   is exactly what broke every OTHER .ql-hidden usage in the app
   previously — the fix must keep both. Any future element that sets its
   own `display` and also needs `.ql-hidden` support needs this same
   compound-selector treatment, not just the generic rule alone. */
.ql-hidden {
  display: none;
}

/* overflow-x:hidden on both html and body (needed on both — the root
   scrolling element differs by browser) guards against #ql-contact's
   100vw full-bleed background: 100vw includes the scrollbar's own width
   on non-overlay-scrollbar platforms, which can otherwise open a sliver
   of real horizontal scroll wider than the visible viewport. Previously
   only applied under the mobile breakpoint (its own sub-2px .ql-page
   rounding fix, see that comment further down) — extended globally now
   that a real desktop-width full-bleed element exists too. */
html {
  overflow-x: hidden;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--ql-bg);
  color: var(--ql-text);
  font-family: 'VT323', 'Courier New', monospace;
  font-size: 20px;
  padding: 3rem 1rem;
  overflow-x: hidden;
}

/* Whole-viewport CRT scanline, not per-panel — a single overlay reads as
   "looking at the screen through glass," rather than needing the z-index
   arbitration the old per-panel version required (every .ql-panel child
   needed its own explicit position/z-index to paint above it). Kept
   restrained (mix-blend-mode + low opacity) per the brief: enough texture
   to read as a real CRT, not so much it becomes the dominant visual. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 999;
  background: repeating-linear-gradient(
    0deg,
    rgba(0, 0, 0, 0.25) 0px,
    rgba(0, 0, 0, 0.25) 1px,
    transparent 1px,
    transparent 3px
  );
  mix-blend-mode: multiply;
}

/* Data Export opts out of the page-wide scanline (explicit staff-tool
   readability request — the horizontal lines were making body text
   harder to read) but keeps it in the nav bar, so the topbar still
   matches every other page. Scoped to this one page via a body class,
   not a global change — every other page keeps the full-viewport
   version above untouched. */
body.ql-no-page-scanline::before {
  content: none;
}

/* .ql-topbar's own scanline layer, not scoped to ql-no-page-scanline like
   the opt-out above — .ql-topbar sits at z-index 1001 (see its own rule),
   above the page-wide scanline's z-index 999, so its opaque panel
   background was silently hiding the CRT texture across the whole nav bar
   on every page (real bug, caught live on index.html) once the topbar was
   bumped above 999 to fix an unrelated content-painting-through-the-nav
   issue. Repainting the same gradient locally, above the topbar's own
   background, restores it there regardless of whether the rest of the
   page keeps or opts out of the whole-viewport version. */
.ql-topbar::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    0deg,
    rgba(0, 0, 0, 0.25) 0px,
    rgba(0, 0, 0, 0.25) 1px,
    transparent 1px,
    transparent 3px
  );
  mix-blend-mode: multiply;
}

.ql-page {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Single terminal-window frame — replaces the old 3-nested-div arcade
   bezel (outer black ring / green ring / panel). A plain CSS double
   border already miters correctly at all 4 corners with zero extra
   markup; the earlier clip-path/offset-shadow approach was exactly the
   "flat CSS shapes faking pixel art" the redesign was asked to fix. */
.ql-panel {
  width: 90%;
  max-width: 420px;
  border: 4px double var(--ql-accent);
  background: var(--ql-panel);
  box-shadow:
    0 0 40px rgba(57, 255, 138, 0.12),
    inset 0 0 60px rgba(57, 255, 138, 0.04);
  padding: 0 0 1.75rem;
  text-align: center;
}

/* Wider variant for the two-column main console (login/forgot/reset stay
   at the narrow default above — they only ever have one column). */
.ql-panel--wide {
  max-width: 1320px;
}

.ql-titlebar {
  padding: 0.9rem 1.5rem;
  margin: 0 0 1.5rem;
  border-bottom: 4px double var(--ql-accent);
  text-align: left;
}

/* Single-row header (2026-07-22, direct request) — combines what used to
   be two stacked rows (title bar, then a separate .ql-nav row below) under
   one divider instead of two: the divider used to sit only under the
   title, leaving the nav row's own bottom edge unmarked. .ql-nav is now a
   flex child living inside .ql-titlebar rather than a sibling; the divider
   stays panel-width (not full-bleed), by direct request. Scoped to
   .ql-titlebar--with-nav (index.html/profile.html/boss.html only) — a
   real bug, caught 2026-07-23: this used to be the base .ql-titlebar rule,
   which also broke login/forgot-password/reset-password's title+cursor+
   subtitle layout (no nav there at all), since they share the same
   .ql-titlebar class but a different internal structure. */
.ql-titlebar--with-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* Login's titlebar moved entirely outside .ql-panel in the markup (direct
   request: the logo/subtitle should sit free of any frame, not just lose
   the internal divider — the double-border bottom edge combined with
   .ql-panel's own double-border sides was reading as a second frame boxing
   them in). As a sibling of .ql-panel instead of a child, it no longer
   inherits the panel's width, so it gets its own matching width/max-width
   here to keep the same left edge as the panel below it; border-bottom and
   the default bottom margin are dropped in favor of a slightly larger gap
   before the now-visually-separate panel. Scoped to this one page via
   body.ql-login-page — every other .ql-panel page (Today's Quest, Profile,
   Boss Battle) keeps the titlebar inside the panel, framed, unchanged. */
body.ql-login-page .ql-titlebar {
  width: 90%;
  max-width: 420px;
  margin: 0 0 1.75rem;
  border-bottom: none;
}

/* .ql-panel's base padding is `0 0 1.75rem` (top: 0) — fine when
   .ql-titlebar used to be its first child, since the titlebar's own
   padding provided the visual top gap. Now that the "QuestLog Login"
   heading is the panel's first child (titlebar moved out, above), it sat
   right up against the top border with nothing but the border's own 4px
   for spacing — direct request for more room. */
body.ql-login-page .ql-panel {
  padding-top: 1.75rem;
}

.ql-titlebar__name {
  font-size: 1.4rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 10px rgba(57, 255, 138, 0.7);
}

.ql-titlebar__meta {
  margin: 0.35rem 0 0;
  font-size: 1.05rem;
  color: var(--ql-muted);
}

/* Hard on/off blink (steps(1)), not a fade — a fade reads as a soft UI
   transition; a real terminal cursor snaps. */
/* Empty span, no explicit height — with nothing inside it and no height
   set, an inline-block can render at effectively zero visible area
   regardless of background color or animation state. 0.85em/vertical-align
   makes it read as a proper block cursor sitting on the text baseline,
   matching the surrounding title text's proportions. */
.ql-cursor {
  display: inline-block;
  width: 0.55em;
  height: 0.85em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--ql-accent);
  color: var(--ql-bg);
  animation: ql-blink 1.1s steps(1) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .ql-cursor {
    animation: none;
  }
}

@keyframes ql-blink {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

h1,
.ql-heading {
  font-family: 'VT323', monospace;
  font-size: 1.5rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.6);
  margin: 0 0 1.25rem;
  padding: 0 1.5rem;
}

/* Two-column main console layout (Today's Quest left, logging sections
   right). */
.ql-layout {
  display: grid;
  grid-template-columns: 0.85fr 2.15fr;
  padding: 0 1.75rem;
}

.ql-column--quest {
  text-align: center;
  padding-right: 1.75rem;
}

.ql-column--log {
  text-align: left;
  padding-left: 1.75rem;
  border-left: 1px solid rgba(57, 255, 138, 0.35);
}

/* 3 log sections side-by-side. minmax(0, 1fr), not a bare 1fr — same
   /impeccable adapt finding (2026-07-28) as .ql-layout-c's own grid
   above: a bare 1fr track can't shrink below its content's min-content
   width, so at narrower desktop widths the 3 sections' combined
   min-content forced this grid (and everything containing it) wider
   than its own column, spilling out to the right. */
.ql-log-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* Profile Page's grid: the first column (avatar, name, Level/Stage chip,
   Level Gauge, Change Character) needs more room than `auto` content-sizing
   gave it (the 192px avatar was the widest thing measured, which cramped
   the gauge and its caption once they were added — direct feedback,
   2026-07-28). A fixed width gives it a comfortable, stable size instead.
   Compound selector (not source-order-dependent) since it must outrank the
   equal-specificity .ql-log-grid rule above. */
.ql-log-grid.ql-profile-grid {
  grid-template-columns: 320px 1fr 1fr;
}

/* Grid stretches all 3 sections to match the tallest (Meals & Snacks has
   more fields than Power-Ups or Obstacles) — flex column + margin-top:auto
   on each section's final action button anchors it to the bottom instead
   of leaving dead space trailing after a short section's last field.
   align-items:flex-start keeps every button left-aligned consistently
   (the earlier version only special-cased the *last* button, so the first
   "Search" button in Meals & Snacks fell back to the container's default
   stretch behavior and misaligned) — form fields/checkbox groups need an
   explicit align-self:stretch to stay full-width under that same rule. */
.ql-log-grid .ql-section {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.ql-log-grid .ql-section .ql-button:last-of-type {
  margin-top: auto;
}

.ql-log-grid .ql-section:first-child {
  padding-left: 0;
}

.ql-log-grid .ql-section:last-child {
  padding-right: 0;
}

.ql-log-grid .ql-section:not(:first-child) {
  border-left: 1px solid rgba(57, 255, 138, 0.35);
}

/* .ql-section--center (the Boss Page's boss-figure section, the only
   :first-child that's centered rather than left-aligned) needs two
   overrides against the left-aligned-by-default rules above:
   (1) symmetric padding — the :first-child rule zeroing only the left
   side pushed its content off-true, shifted toward the removed edge;
   (2) align-items:center — .ql-log-grid .ql-section's own
   align-items:flex-start controls flex-item cross-axis position and wins
   over text-align:center (they're unrelated properties), so the section's
   own children (title, avatar, health bar) were hugging the left edge of
   the column regardless of text-align. Compound selector to at least
   match :first-child's specificity, placed after it so source order also
   favors this override. */
.ql-log-grid .ql-section.ql-section--center {
  align-items: center;
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}

/* "Tiled terminal panes" (Direction B, desktop-only first pass —
   2026-07-22, mockup-approved, replacing an earlier Direction A "open
   floor plan" pass tried first). Today's Quest read as one dense box;
   this leans into the terminal identity rather than away from it — a
   real multiplexer (tmux/screen) layout: each zone (quest, meals,
   power-ups, obstacles) becomes its own bounded pane, separated by thin
   bright seams instead of one thick double border wrapping everything.
   The seam trick is the classic CSS one: the grid container gets
   `gap` + a `background` in the seam color, each pane gets its own
   opaque `--ql-bg` fill, so the gap itself is what shows the seam color
   through — no border rules needed on the panes at all. Titlebar + the
   [ Today's Quest ]/[ Profile ]/[ Boss Battle ] nav row stay outside the
   tiled grid, unchanged. Each pane also gets a small quiet "filename" tab
   (`.ql-pane-tab`, decorative, `aria-hidden="true"`) — a genuine
   terminal-native detail from the mockup, not just decoration for its own
   sake. Scoped to `body.ql-layout-b` (index.html only — Profile/Boss
   Battle keep the boxed panel unchanged) and `min-width: 769px` so mobile
   is completely untouched for now, matching the "desktop first" scope of
   this request; the existing `max-width: 768px` block below still
   collapses this same markup to a single stacked column exactly as
   before. `.ql-section-heading`'s `>` prefix (`::before`, above) is
   untouched by any of this — still every log section's title. */
@media (min-width: 769px) {
  body.ql-layout-b .ql-panel {
    border: none;
    background: transparent;
    box-shadow: none;
  }

  body.ql-layout-b .ql-layout {
    grid-template-columns: 280px 1fr;
    gap: 3px;
    background: rgba(57, 255, 138, 0.35);
    padding: 3px;
  }

  body.ql-layout-b .ql-column--quest {
    padding-right: 0;
    padding: 1.5rem;
    background: var(--ql-bg);
  }

  body.ql-layout-b .ql-column--log {
    padding-left: 0;
    border-left: none;
  }

  body.ql-layout-b .ql-log-grid {
    height: 100%;
    gap: 3px;
    background: rgba(57, 255, 138, 0.35);
  }

  body.ql-layout-b .ql-log-grid .ql-section {
    background: var(--ql-bg);
    padding: 1.25rem;
  }

  body.ql-layout-b .ql-log-grid .ql-section:first-child,
  body.ql-layout-b .ql-log-grid .ql-section:last-child {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }

  body.ql-layout-b .ql-log-grid .ql-section:not(:first-child) {
    border-left: none;
  }

  body.ql-layout-b .ql-pane-tab {
    display: block;
    text-align: left;
    font-family: system-ui, sans-serif;
    font-size: 0.68rem;
    letter-spacing: 0.05em;
    color: var(--ql-muted);
    margin-bottom: 0.75rem;
  }
}

/* Hidden outside the tiled layout (and below its 769px floor) — this tab
   only makes sense as a per-pane "filename" label in that specific
   treatment; every other context (mobile, Profile, Boss Battle) never
   renders it. */
.ql-pane-tab {
  display: none;
}

/* Removes the double-border/fill/glow .ql-panel box on Profile and Boss
   Battle too (direct request 2026-07-22, following Today's Quest's own
   unboxing) — titlebar + the [ Today's Quest ]/[ Profile ]/[ Boss Battle ]
   nav row stay, only the frame around them and the page content below
   goes. Originally desktop-only, extended to mobile the same day per
   direct follow-up request — unconditional now, no media query, so both
   pages are unboxed at every viewport width. Scoped via `body.ql-unboxed`
   (added to profile.html and boss.html's <body>) rather than reusing
   `body.ql-layout-c` directly — these two pages don't have Today's
   Quest's quest/log grid to restructure, just the box itself to
   remove. */
body.ql-unboxed .ql-panel {
  border: none;
  background: transparent;
  box-shadow: none;
}

/* "Open floor plan" (Direction A, desktop-only — 2026-07-22, rebuilt
   after being tried first, replaced by Direction B, and now brought back
   for a side-by-side comparison against Direction C). Drops the
   .ql-panel double-border/fill/glow for this page specifically (titlebar
   + nav row stay) and turns the quest/log divider lines and the 3 log
   sections' shared dividers into real gaps with each section getting its
   own soft border, so they read as independent open panels instead of
   one continuous grid. Scoped to `body.ql-layout-a` + `min-width: 769px`,
   same mobile/Profile/Boss-Battle exclusions as Direction B. */
@media (min-width: 769px) {
  body.ql-layout-a .ql-panel {
    border: none;
    background: transparent;
    box-shadow: none;
  }

  body.ql-layout-a .ql-layout {
    gap: 2.5rem;
  }

  body.ql-layout-a .ql-column--quest {
    padding-right: 0;
  }

  body.ql-layout-a .ql-column--log {
    padding-left: 0;
    border-left: none;
  }

  body.ql-layout-a .ql-log-grid {
    gap: 1.75rem;
  }

  body.ql-layout-a .ql-log-grid .ql-section {
    border: 1px solid rgba(57, 255, 138, 0.25);
    background: rgba(57, 255, 138, 0.02);
    padding: 1.25rem;
  }

  body.ql-layout-a .ql-log-grid .ql-section:first-child,
  body.ql-layout-a .ql-log-grid .ql-section:last-child {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }

  body.ql-layout-a .ql-log-grid .ql-section:not(:first-child) {
    border-left: 1px solid rgba(57, 255, 138, 0.25);
  }
}

/* Removes the double-border/fill/glow .ql-panel box for Today's Quest —
   unconditional (no media query) same as Profile/Boss Battle's own
   `.ql-unboxed` rule above, extended to mobile 2026-07-22 per direct
   follow-up request. Pulled out of Direction C's own desktop-only block
   below since this part isn't desktop-specific anymore, unlike the rest
   of that layout (column proportions, log-grid gap/borders), which stays
   gated to `min-width: 769px` — mobile still stacks quest/log into a
   single column via the existing `max-width: 768px` block, just without
   the box around it now. */
body.ql-layout-c .ql-panel {
  border: none;
  background: transparent;
  box-shadow: none;
}

/* "Persistent sidebar app-shell" (Direction C, desktop-only —
   2026-07-22, mockup-approved and picked as the keeper after comparing
   against A/B live; widened from an initial 240px to 300px same day per
   direct follow-up). Unlike A/B, this changes the actual column
   proportions, not just how the dividers are drawn: the quest column
   becomes a narrow fixed-width rail (avatar + stat bars only, always
   visible) instead of a 0.85fr share of the row, and the log column
   takes the rest of the width outright — the
   3 log panels get noticeably more room since they're no longer sharing
   a fixed split with the quest column. Log panels reuse Direction A's
   open-panel-with-gap treatment (bordered, gapped, no shared dividers)
   for visual consistency between the two; only .ql-layout's column split
   and the quest rail's own border differ. Scoped to `body.ql-layout-c` +
   `min-width: 769px`, same mobile exclusion (the box removal itself is
   now unconditional, see above — only the column-proportion changes stay
   desktop-only). */
@media (min-width: 769px) {
  /* minmax(0, 1fr), not a bare 1fr — found via /impeccable adapt
     (2026-07-28): a bare 1fr track has an implicit minmax(auto, 1fr),
     and "auto" resolves to the log-grid's content min-content width,
     which a plain 1fr can't shrink below. Between ~769-999px that
     min-content floor was wider than the viewport, forcing the whole
     .ql-page/.ql-panel--wide box wider too — centered by body's flex
     layout, so it clipped symmetrically off both edges instead of
     just the log column overflowing on its own. minmax(0, 1fr) lets
     the track actually shrink to the column's real 578px share instead
     of demanding its children's full natural width. */
  body.ql-layout-c .ql-layout {
    grid-template-columns: 300px minmax(0, 1fr);
    gap: 2.5rem;
  }

  body.ql-layout-c .ql-column--quest {
    padding-right: 1.5rem;
    border-right: 1px solid rgba(57, 255, 138, 0.3);
  }

  body.ql-layout-c .ql-column--log {
    padding-left: 0;
    border-left: none;
  }

  body.ql-layout-c .ql-log-grid {
    gap: 1.75rem;
  }

  body.ql-layout-c .ql-log-grid .ql-section {
    border: 1px solid rgba(57, 255, 138, 0.25);
    background: rgba(57, 255, 138, 0.02);
    padding: 1.25rem;
  }

  body.ql-layout-c .ql-log-grid .ql-section:first-child,
  body.ql-layout-c .ql-log-grid .ql-section:last-child {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
  }

  body.ql-layout-c .ql-log-grid .ql-section:not(:first-child) {
    border-left: 1px solid rgba(57, 255, 138, 0.25);
  }
}

.ql-field {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  margin-bottom: 1rem;
  text-align: left;
}

.ql-field label {
  margin-bottom: 0.25rem;
  font-size: 1.05rem;
  color: var(--ql-muted);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.ql-field input,
.ql-field textarea {
  width: 100%;
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
}

/* Account page's Change Password fields — .ql-field's own `width: 100%`
   stretches across .ql-plain-page's full ~1320px width, way wider than a
   password field needs (login/reset-password's fields look reasonably
   sized only because .ql-panel itself caps at 420px). Capped here instead
   of changing .ql-field globally, which other, genuinely wide fields on
   other pages rely on. */
#ql-change-password-form input,
#ql-change-password-form .ql-password-field {
  max-width: 24rem;
}

.ql-field textarea {
  resize: vertical;
}

/* Login's email/password fields inset from the panel's edges (direct
   request: they read as too wide, spanning nearly the full panel). Padding
   on the form (rather than each .ql-field) narrows the label and input
   together in one place, and lines up with the heading's/titlebar's own
   1.5rem horizontal padding above it. Scoped to this one form only — every
   other .ql-field on the site (Settings, meal search, etc.) stays full
   width. */
#ql-login-form {
  padding: 0 1.5rem;
}

/* Real bug, unrelated to mobile — caught live while screenshot-comparing
   the auth pages during the mobile adaptation pass, confirmed identical
   at desktop width so not a regression from that work. #ql-login-form
   above got this exact treatment already; #ql-forgot-form/#ql-reset-form
   never did, so their Email/new-password fields sat flush against the
   panel's edges with no horizontal inset at all, unlike every other
   element on these pages (heading, titlebar) which already have 1.5rem
   padding built into their own rules. */
#ql-forgot-form,
#ql-reset-form {
  padding: 0 1.5rem;
}

/* Scanline removed for just the Email/Password fields, not the whole page
   (revised from an earlier page-wide `ql-no-page-scanline` opt-out, which
   the user found removed too much — the rest of the login page, titlebar
   and panel border included, should keep the CRT texture). Same
   z-index-lift technique used for the How This Works section: body::before
   is `position: fixed; z-index: 999`, and neither .ql-page nor .ql-panel
   establish their own stacking context, so giving these two fields
   `position: relative; z-index: 1000` is enough to paint them above the
   overlay without touching anything else on the page. Login has no
   .ql-topbar, so unlike How This Works this doesn't risk the same
   content-painting-through-the-nav bug — see the fix on .ql-topbar itself
   if a fixed nav is ever added to this page. */
#ql-login-form .ql-field {
  position: relative;
  z-index: 1000;
}

/* Bracketed command style — `[ LOG POWER-UPS ]` — instead of a filled
   rectangle with an offset "pixel" shadow. Reverse-video on hover/focus
   (the container's own green becomes the button's background) is the
   direct Norton Commander/htop reference from the confirmed brief; the
   old hard-offset-shadow press mechanic is dropped along with it. */
/* Padding tried at 0.85rem 0 to clear the 44px touch-target minimum
   (`/impeccable audit` P1) — reverted to the original 0.5rem per direct
   feedback: the taller buttons read as visually wrong, and that verdict
   wins over the audit's numeric target. The touch-target gap is
   documented as a known, deliberately-accepted tradeoff below. */
.ql-button {
  display: inline-block;
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: transparent;
  color: var(--ql-accent);
  border: none;
  border-radius: 0;
  padding: 0.5rem 0;
  cursor: pointer;
  text-shadow: 0 0 2px rgba(57, 255, 138, 0.6);
}

/* Dutch button labels run longer than their English equivalents on
   average (compound words, e.g. "Maaltijd Registreren" vs "Log Meal") —
   enough that 2 of the Quest log grid's 3 columns ("Maaltijd
   Registreren", "Power-Ups Registreren") wrapped onto a second line at
   ordinary desktop widths, splitting the bracket punctuation onto its own
   line (real bug, reported live with a screenshot). A small, uniform
   reduction applied to every Dutch button (not just these two) reclaims
   enough width to keep labels on one line, without creating a size
   mismatch against other buttons on the same page. */
html[lang='nl'] .ql-button {
  font-size: 1rem;
  letter-spacing: 0.05em;
}

.ql-button::before {
  content: '[ ';
}

.ql-button::after {
  content: ' ]';
}

.ql-button:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
  text-shadow: none;
}

/* Current-page nav button (.ql-nav's [ TODAY'S QUEST ]/[ PROFILE ]/
   [ BOSS BATTLE ] trio) stays in the reverse-video fill permanently,
   instead of only ever showing it on hover — direct request, so it's
   clear at a glance which of the 3 pages you're already on. Marked via
   `aria-current="page"` on that page's own link (one per page, in
   index.html/profile.html/boss.html) rather than a new class — it's the
   semantically correct attribute for "this is the current page in a set
   of nav links" and gets announced by screen readers for free. */
.ql-button[aria-current='page'] {
  background: var(--ql-accent);
  color: var(--ql-bg);
  text-shadow: none;
}

/* Deferred decision, not yet applied by default: grows .ql-button to a real
   44px touch target specifically on coarse-pointer (touch) devices, without
   touching the ~34px desktop/mouse look you already approved (see the
   0.5rem-padding comment above — reverting the flat 44px change once was a
   real, deliberate call, not an oversight). pointer:coarse only matches
   real touch input, so this is inert on desktop regardless. Shown live for
   comparison before deciding whether to keep it. */
@media (pointer: coarse) {
  .ql-button {
    padding: 0.85rem 0;
  }
}

.ql-button:active {
  filter: brightness(0.85);
}

.ql-button:disabled {
  color: var(--ql-muted);
  text-shadow: none;
  cursor: not-allowed;
}

.ql-button:disabled:hover {
  background: transparent;
  color: var(--ql-muted);
}

/* Signal Cyan, not the browser-default blue outline — on-brand per the
   Two-Accent Rule (cyan = informational/status) and better contrast
   against this dark palette than the default. */
.ql-button:focus-visible,
.ql-field input:focus-visible,
.ql-field textarea:focus-visible,
.ql-select:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-checkbox input:focus-visible + .ql-checkbox-box {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-error {
  color: var(--ql-accent-cyan);
  font-family: 'VT323', monospace;
  margin-top: 1rem;
  min-height: 1.5rem;
}

.ql-success {
  color: var(--ql-accent);
  font-family: 'VT323', monospace;
  margin-top: 1rem;
}

.ql-success a {
  color: var(--ql-accent);
}

/* New Food Badge celebration (2026-07-20, /impeccable delight, approved
   design: "Terminal print-out") — the earned name types itself out like a
   real command completing, ending in the app's own .ql-cursor block cursor
   (same class/animation as the page titlebar's), rather than a generic
   toast/confetti pop. Stays up until the page reloads or the next meal is
   logged (see app.js's hideBadgeToast) — no auto-dismiss timer, per direct
   request.

   Moved 2026-08-02 from inside the Meals & Snacks column to a full-width
   banner between .ql-log-grid and Current Mission (.ql-boss-watch) — a
   long badge name (e.g. "BELL PEPPER") plus its icon and label text didn't
   comfortably fit in a 1/3-width column and visibly overflowed into the
   Power-Ups column next to it. Same margin-top as .ql-boss-watch for
   consistent rhythm between the two. min-width:0 on the line (a flex item
   of this flex row) plus overflow/ellipsis handles any text that's still
   too long even at full width, rather than relying on width alone. */
.ql-badge-toast {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1.5rem;
  padding: 0.75rem 1rem;
  background: var(--ql-bg);
  border: 1px solid var(--ql-accent);
}

.ql-badge-toast.ql-hidden {
  display: none;
}

.ql-badge-toast.ql-badge-toast--entering {
  animation: ql-badge-toast-in 0.25s ease-out;
}

@keyframes ql-badge-toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

.ql-badge-toast__line {
  min-width: 0;
  margin: 0;
  font-size: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ql-badge-toast__prompt {
  color: var(--ql-accent);
}

.ql-badge-toast__text {
  color: var(--ql-text);
  text-transform: uppercase;
}

@media (prefers-reduced-motion: reduce) {
  .ql-badge-toast.ql-badge-toast--entering {
    animation: none;
  }
}

.ql-text-link {
  color: var(--ql-accent-cyan);
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
}

.ql-text-link:hover {
  text-decoration: none;
}

/* A <button> that reads as a text link — for actions (not navigation) that
   shouldn't compete visually with a page's primary .ql-button actions, e.g.
   the "Can't find it? Log it manually" trigger next to meal search results. */
.ql-link-button {
  display: block;
  background: none;
  border: none;
  padding: 0;
  margin-top: 0.6rem;
  /* Muted, not Signal Cyan (reverted 2026-07-22 direct request) — Cyan is
     reserved for informational/status readouts elsewhere (Two-Accent
     Rule); this is a secondary escape-hatch action, same role as the
     onboarding tour's "Skip tour" text, so it gets the same quiet
     treatment rather than competing with Search/Log Meal. */
  color: var(--ql-muted);
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  text-align: left;
  white-space: nowrap;
  text-decoration: underline;
  cursor: pointer;
}

/* English fits this trigger on one line at the Quest log grid's column
   width; Dutch's longer equivalent ("Kun je het niet vinden? Handmatig
   registreren") doesn't, and nowrap let it overflow past the column into
   Power-Ups next door instead of wrapping (real bug, reported live with a
   screenshot). Scoped to Dutch only so English keeps its one-line look. */
html[lang='nl'] .ql-link-button {
  white-space: normal;
}

.ql-link-button:hover,
.ql-link-button:focus-visible {
  text-decoration: none;
}

.ql-avatar-wrap {
  margin-bottom: 1.5rem;
  filter: drop-shadow(0 0 6px rgba(57, 255, 138, 0.55));
}

/* Single static avatar/boss figure, same look regardless of stat levels
   (PRD Section 3 — no avatar states yet). */
.ql-avatar {
  width: 128px;
  height: 128px;
  image-rendering: pixelated;
}

/* Profile Page shows the avatar larger than Today's Quest/Boss Page — still
   a multiple of the source art's 64px so pixel edges stay crisp at scale. */
.ql-avatar--large {
  width: 192px;
  height: 192px;
}

/* Day Streak / Level & Stage chips (PRD Section 16.1/16.3), sitting between
   the avatar and the stat bars on Today's Quest. Same VT323/uppercase
   treatment as .stat-bar__label rather than a new typographic voice. */
.ql-progress-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  margin-bottom: 1.25rem;
}

/* position: relative + no overflow clip so the Day Streak flash's glow
   ring (GSAP, app.js) can expand outward past the chip's own edges. */
.ql-progress-chip {
  position: relative;
}

/* Day Streak celebration (2026-07-27) — deliberately no modal, matching
   the "no ceremony for frequent events" convention Power-Ups logging
   already established. GSAP animates one of these in per flash, expanding
   outward from the chip, then removes it — see app.js's flashStreakChip. */
.ql-progress-chip__ring {
  position: absolute;
  left: 0.2rem;
  top: 50%;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  border-radius: 50%;
  border: 2px solid var(--ql-accent-cyan);
  pointer-events: none;
}

.ql-progress-chip__label {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Cyan, per the Two-Accent Rule (DESIGN.md) — informational readout, same
   role .stat-bar__value already plays for the stat numbers next to it. */
.ql-progress-chip__value,
.ql-progress-chip__stage {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.6);
}

/* inline-block so app.js's flashStreakChip scale-pop (GSAP) renders
   reliably — a plain inline element can apply transforms inconsistently. */
.ql-progress-chip__value {
  display: inline-block;
}

/* Level Gauge (Profile Page, PRD 16.3 follow-up) — a plain track + fill
   flanked by two numbers, adapted from a user-supplied reference image
   (originally a dot-matrix HUD gauge with chevron end-caps and a boundary
   marker). Simplified 2026-07-28, direct feedback that the chevrons and
   boundary marker read as crowded/noisy on the narrower character column —
   both removed; the numbers alone flank the track now. All-green now too
   (was a green fill with Signal Cyan numbers/marker per the Two-Accent
   Rule) — direct feedback to make "the progress bar and the text around
   it" one consistent color, Terminal Green, same as the stamina bar and
   the Level-Up celebration brackets. */
.ql-level-gauge {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  max-width: 320px;
  margin: 1rem auto 0;
}

.ql-level-gauge__number {
  font-family: 'VT323', monospace;
  font-size: 1.2rem;
  color: var(--ql-accent);
  text-shadow: 0 0 4px rgba(57, 255, 138, 0.6);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.ql-level-gauge__track {
  position: relative;
  flex: 1;
  height: 1.3rem;
  border-top: 1px solid var(--ql-muted);
  border-bottom: 1px solid var(--ql-muted);
}

.ql-level-gauge__fill {
  position: absolute;
  inset: 0;
  width: 0;
  background: var(--ql-accent);
  box-shadow: 0 0 8px rgba(57, 255, 138, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: width 0.3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .ql-level-gauge__fill {
    transition: none;
  }
}

/* Max level (15) — dual green->cyan glow plus a label baked into the fill
   itself, instead of a fraction that would have nothing to divide by.
   Still purely positive, no dead-end/warning styling (CLAUDE.md #9). */
.ql-level-gauge__fill.is-maxed {
  background: linear-gradient(90deg, var(--ql-accent), var(--ql-accent-cyan));
  box-shadow: 0 0 12px rgba(54, 255, 255, 0.8);
}

.ql-level-gauge__maxed-label {
  font-family: 'VT323', monospace;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-bg);
}

/* Smaller than the first pass (1.6rem) — direct feedback that the big
   caption felt crowded once the chip/gauge/name all sit in the same
   narrow column. Only the lifetime-points numerator is tinted/glowed;
   "/ 140" stays plain text color so the earned number draws the eye. */
.ql-level-gauge__caption {
  text-align: center;
  margin-top: 0.5rem;
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  color: var(--ql-text);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

.ql-level-gauge__caption strong {
  color: var(--ql-accent);
  text-shadow: 0 0 4px rgba(57, 255, 138, 0.6);
}

/* Editable in-game character name, shown under the avatar on the Profile
   Page (distinct from the client's real `name`, never shown/edited here). */
.ql-character-name {
  margin-top: 1rem;
}

.ql-character-name__button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  letter-spacing: 0.05em;
  background: transparent;
  border: none;
  color: var(--ql-text);
  cursor: pointer;
  padding: 0.25rem 0.4rem;
}

/* Out-specificities .ql-character-name__button's own `display` — same
   .ql-hidden-vs-component-display issue as .ql-character-name__form below. */
.ql-character-name__button.ql-hidden {
  display: none;
}

.ql-character-name__text {
  color: var(--ql-accent);
  text-shadow: 0 0 4px rgba(57, 255, 138, 0.5);
}

.ql-character-name__text--placeholder {
  color: var(--ql-muted);
  text-shadow: none;
  font-style: italic;
}

.ql-character-name__pencil {
  font-size: 1rem;
  color: var(--ql-muted);
}

.ql-character-name__button:hover .ql-character-name__text,
.ql-character-name__button:focus-visible .ql-character-name__text {
  color: var(--ql-accent-cyan);
}

.ql-character-name__button:hover .ql-character-name__pencil,
.ql-character-name__button:focus-visible .ql-character-name__pencil {
  color: var(--ql-accent-cyan);
}

.ql-character-name__button:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-character-name__form {
  display: inline-block;
}

/* Out-specificities .ql-character-name__form's own `display` so toggling
   .ql-hidden on this element actually hides it — see the .ql-hidden comment
   above for why the generic utility alone isn't enough once a component
   sets its own `display`. */
.ql-character-name__form.ql-hidden {
  display: none;
}

.ql-character-name__input {
  width: 14rem;
  max-width: 60vw;
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  text-align: center;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
}

.ql-character-name__input:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

/* Character/avatar picker (Profile Page, new 2026-07-17) — a dropdown
   panel anchored to the avatar, built from 3 rounds of mockups: the
   layout (grid + live preview pane), which locked-slot treatment ("B" —
   a faint dimmed placeholder + small caption, no padlock icon, since a
   literal padlock reads too much like generic game-UI chrome for this
   app's actual "real terminal system" identity), and the frame (the
   standard double-border Terminal Window, same as every other window in
   the app — not the sci-fi HUD treatment from the guidance/victory
   pop-ups, which stays a one-off exception for those two components).
   .ql-avatar-picker is the positioning anchor for the panel below. */
.ql-avatar-picker {
  position: relative;
  display: inline-block;
}

.ql-avatar-picker__trigger {
  display: block;
  margin: 0.5rem auto 0;
  font-family: 'VT323', monospace;
  font-size: 1rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ql-muted);
  background: transparent;
  border: none;
  border-bottom: 1px dotted var(--ql-muted);
  cursor: pointer;
  padding: 0;
}

.ql-avatar-picker__trigger:hover,
.ql-avatar-picker__trigger:focus-visible {
  color: var(--ql-accent);
  border-bottom-color: var(--ql-accent);
}

.ql-avatar-picker__trigger:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 3px;
}

.ql-avatar-picker__panel {
  position: absolute;
  left: calc(100% + 1.5rem);
  top: 50%;
  transform: translateY(-50%);
  z-index: 20;
  width: 600px;
  background: linear-gradient(180deg, #0c2e1d 0%, #02100a 100%);
  border: 1px solid var(--ql-accent);
  box-shadow: 0 0 30px rgba(57, 255, 138, 0.12);
  padding: 2.25rem 2.5rem;
  text-align: left;
}

/* 4 corner brackets in Signal Cyan — the "Frame 2" treatment from the
   mockup round: a lighter hairline border than the app's usual
   double-border Terminal Window, with corner accents reading like a
   targeting reticle instead. */
.ql-avatar-picker__panel-corner {
  position: absolute;
  width: 14px;
  height: 14px;
  border-color: var(--ql-accent-cyan);
}

.ql-avatar-picker__panel-corner--tl {
  top: -1px;
  left: -1px;
  border-top: 2px solid;
  border-left: 2px solid;
}

.ql-avatar-picker__panel-corner--tr {
  top: -1px;
  right: -1px;
  border-top: 2px solid;
  border-right: 2px solid;
}

.ql-avatar-picker__panel-corner--bl {
  bottom: -1px;
  left: -1px;
  border-bottom: 2px solid;
  border-left: 2px solid;
}

.ql-avatar-picker__panel-corner--br {
  bottom: -1px;
  right: -1px;
  border-bottom: 2px solid;
  border-right: 2px solid;
}

/* Reuses the avatar picker's "Frame 2" chrome (gradient background, hairline
   border, cyan corner brackets) on another modal, per direct request to
   match the Choose Your Character look — layered on top of .ql-modal (kept
   for its padding/text-align) rather than duplicating those rules.
   Combined `.ql-modal.ql-frame2` selector (not just `.ql-frame2` alone) so
   this reliably outranks .ql-modal's own background/border/box-shadow
   regardless of which rule appears later in the file — the same
   equal-specificity/source-order trap this codebase's .ql-hidden history
   already warns about. .ql-avatar-picker__panel-corner's absolutely-
   positioned corner spans need this to be their positioned ancestor, hence
   position: relative here. */
.ql-modal.ql-frame2 {
  position: relative;
  background: linear-gradient(180deg, #0c2e1d 0%, #02100a 100%);
  border: 1px solid var(--ql-accent);
  box-shadow: 0 0 30px rgba(57, 255, 138, 0.12);
}

.ql-avatar-picker__heading {
  font-family: 'VT323', monospace;
  font-size: 1.5rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.6);
  margin: 0 0 1.25rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.4);
}

.ql-avatar-picker__body {
  display: flex;
  gap: 1.75rem;
}

.ql-avatar-picker__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
  flex: 0 0 auto;
}

.ql-avatar-picker__tile {
  width: 76px;
  height: 76px;
  background: var(--ql-bg);
  border: 2px solid rgba(57, 255, 138, 0.4);
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  position: relative;
}

.ql-avatar-picker__tile:hover,
.ql-avatar-picker__tile:focus-visible {
  border-color: var(--ql-accent);
}

.ql-avatar-picker__tile:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-avatar-picker__tile-img {
  width: 58px;
  height: 58px;
  image-rendering: pixelated;
}

.ql-avatar-picker__tile--selected {
  border: 2px solid var(--ql-accent);
  box-shadow:
    0 0 12px rgba(57, 255, 138, 0.4),
    inset 0 0 10px rgba(57, 255, 138, 0.15);
}

.ql-avatar-picker__tile--selected::after {
  content: '✓';
  position: absolute;
  top: -8px;
  right: -8px;
  width: 20px;
  height: 20px;
  background: var(--ql-accent);
  color: var(--ql-bg);
  font-size: 0.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'VT323', monospace;
}

/* Locked slots are plain, non-interactive divs (no click handler, no
   image — there's no real art for a character that doesn't exist yet).
   Dashed border + very muted caption, deliberately not a padlock icon. */
.ql-avatar-picker__tile--locked {
  border: 1px dashed rgba(107, 156, 134, 0.35);
  background: transparent;
  cursor: default;
}

.ql-avatar-picker__locked-label {
  font-family: 'VT323', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ql-muted);
  opacity: 0.65;
  text-align: center;
}

.ql-avatar-picker__preview {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-left: 1px dotted rgba(57, 255, 138, 0.35);
  padding-left: 1.75rem;
  text-align: center;
}

.ql-avatar-picker__preview-img {
  width: 160px;
  height: 160px;
  image-rendering: pixelated;
  filter: drop-shadow(0 0 8px rgba(57, 255, 138, 0.4));
  margin-bottom: 0.85rem;
}

.ql-avatar-picker__preview-name {
  font-family: 'VT323', monospace;
  font-size: 1.5rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-text);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 6px rgba(57, 255, 138, 0.5);
  margin: 0 0 0.85rem;
}

.ql-avatar-picker__select {
  margin: 0 auto;
}

/* Displayed-stage picker (PRD 16.2) — a second, smaller tile row below the
   character grid + preview, inside the same picker panel. Unlocked stages
   show the real thumbnail (avatarSrc(previewAvatarKey, stage)); locked ones
   show a plain "?" instead (direct user request, replacing first a dimmed-
   real-art treatment, then a dotted silhouette) — communicates "a character
   exists here, but you haven't earned the reveal" rather than a spoiler. */
.ql-avatar-picker__stage-section {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px dotted rgba(57, 255, 138, 0.4);
}

.ql-avatar-picker__stage-heading {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ql-muted);
  margin: 0 0 0.75rem;
}

.ql-avatar-picker__grid--stage {
  grid-template-columns: repeat(3, 76px);
}

.ql-avatar-picker__tile--stage {
  flex-direction: column;
  gap: 0.15rem;
}

.ql-avatar-picker__stage-lock-label {
  font-family: 'VT323', monospace;
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ql-muted);
}

.ql-avatar-picker__stage-placeholder {
  display: none;
  font-family: 'VT323', monospace;
  font-size: 2rem;
  line-height: 40px;
  height: 40px;
  color: var(--ql-muted);
  opacity: 0.7;
}

.ql-avatar-picker__tile--stage:disabled {
  cursor: default;
  border-color: rgba(107, 156, 134, 0.35);
}

.ql-avatar-picker__tile--stage:disabled .ql-avatar-picker__tile-img {
  display: none;
}

.ql-avatar-picker__tile--stage:disabled .ql-avatar-picker__stage-placeholder {
  display: block;
}

.ql-avatar-picker__tile--stage:disabled:hover,
.ql-avatar-picker__tile--stage:disabled:focus-visible {
  border-color: rgba(107, 156, 134, 0.35);
}

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

.stat-bar {
  text-align: left;
  margin-bottom: 1.25rem;
}

.stat-bar__label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.stat-bar__label {
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Numeric readout next to each label — a stat sitting at 0 previously had
   no legible value anywhere (just an empty track), unreadable for anyone
   glancing at the bar and invisible to screen readers. Cyan, per the
   Two-Accent Rule: this is informational readout, not a primary action. */
.stat-bar__value {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.6);
}

/* "Phosphor Surge" gain pop — fired by app.js's setFill() only on a real
   increase (never on the initial page load or a decrease). steps(), not a
   smooth ease, to match the terminal cursor's own hard on/off blink rather
   than reading as a soft UI transition. */
.stat-bar__value.is-gaining {
  animation: ql-stat-gain-pop 0.3s steps(2);
}

@keyframes ql-stat-gain-pop {
  0%,
  100% {
    transform: scale(1);
    filter: brightness(1);
  }
  50% {
    transform: scale(1.85);
    filter: brightness(1.9);
  }
}

@media (prefers-reduced-motion: reduce) {
  .stat-bar__value.is-gaining {
    animation: none;
  }
}

.stat-bar__track {
  border: 1px solid var(--ql-accent);
  background: var(--ql-bg);
  height: 1.3rem;
}

/* transform: scaleX(), not width — animating width triggers layout on
   every frame; scaleX is compositor-only. transform-origin keeps growth
   anchored to the left edge, matching how width: 0% -> N% used to read. */
.stat-bar__fill {
  position: relative;
  height: 100%;
  width: 100%;
  overflow: hidden;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .stat-bar__fill {
    transition: none;
  }
}

/* Gain sweep + glow, paired with .stat-bar__value's pop above. A plain
   white highlight (not a per-stat color) sweeps across the fill via this
   ::after, so the same rule reads correctly against Stamina's green,
   Focus's cyan, and Health's amber. The glow pulse uses filter:
   brightness() rather than a duplicated per-color box-shadow for the same
   reason — it scales whatever glow color the bar already has. */
.stat-bar__fill::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.85), transparent);
  transform: translateX(-120%);
  pointer-events: none;
}

.stat-bar__fill.is-gaining {
  animation: ql-stat-gain-glow 0.4s ease-out;
}

.stat-bar__fill.is-gaining::after {
  animation: ql-stat-gain-sweep 0.4s ease-out;
}

@keyframes ql-stat-gain-sweep {
  to {
    transform: translateX(120%);
  }
}

@keyframes ql-stat-gain-glow {
  0%,
  100% {
    filter: brightness(1);
  }
  40% {
    filter: brightness(1.7);
  }
}

@media (prefers-reduced-motion: reduce) {
  .stat-bar__fill.is-gaining,
  .stat-bar__fill.is-gaining::after {
    animation: none;
  }
}

.stat-bar--stamina .stat-bar__fill {
  background: var(--ql-accent);
  box-shadow: 0 0 8px rgba(57, 255, 138, 0.8);
}

.stat-bar--focus .stat-bar__fill {
  background: var(--ql-accent-cyan);
  box-shadow: 0 0 8px rgba(54, 255, 255, 0.8);
}

.stat-bar--health .stat-bar__fill {
  background: #f2c14e;
  box-shadow: 0 0 8px rgba(242, 193, 78, 0.7);
}

.ql-section {
  text-align: left;
  padding: 0 1.25rem;
}

/* `>` command-prompt prefix via CSS, not markup — keeps strings.js as the
   single source of truth for the actual text. Scoped to section-level
   headings only; the page/panel-level .ql-heading (e.g. "Today's Quest",
   "QuestLog Login") stays a centered title with no prefix — one tier
   quieter reads as "inside a running session," not "a new command." */
.ql-section-heading {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow: 0 0 2px rgba(57, 255, 138, 0.5);
  margin: 0 0 0.75rem;
}

.ql-section-heading::before {
  content: '> ';
}

.ql-section-placeholder {
  color: var(--ql-muted);
  font-size: 1.05rem;
  margin: 0;
}

.ql-account-data-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin-top: 1rem;
}

/* Account Page — Account Details (Name/Email/Location). Name reuses
   .ql-character-name's existing click-pencil-to-edit component as-is (it's
   already a generic "editable name" widget, not Profile-page-specific);
   Email/Location are plain read-only rows in the same label/value shape. */
.ql-account-detail-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.25);
}

.ql-account-detail-row .ql-character-name {
  margin-top: 0;
}

.ql-account-detail-label {
  flex-shrink: 0;
  width: 8rem;
  color: var(--ql-muted);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-size: 1.05rem;
}

.ql-account-detail-value {
  color: var(--ql-text);
  font-size: 1.1rem;
}

.ql-checkbox-group {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin-bottom: 1rem;
}

/* Vertical padding grows the clickable label area to the 44px touch-target
   minimum (measured 25px tall without it) without enlarging the checkbox
   glyph itself, which would read as oversized. Staff log power-ups on
   tablets during meal times — a stated speed/accuracy requirement, not a
   nice-to-have. Hover reverse-video-tints, echoing the TUI row-selection
   language used elsewhere (buttons, Daily Records rows). */
.ql-checkbox {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.3rem;
  cursor: pointer;
}

.ql-checkbox:hover {
  background: rgba(57, 255, 138, 0.15);
}

/* Visually hidden, not display:none — stays focusable/tabbable and
   readable by screen readers; .ql-checkbox-box below is the on-brand
   glyph a sighted user actually sees and clicks (native checkboxes can't
   be restyled past accent-color, which left a raw OS box in an otherwise
   fully bespoke UI). */
.ql-checkbox input[type='checkbox'] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.ql-checkbox-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  border: 2px solid var(--ql-accent);
  background: var(--ql-panel);
  color: var(--ql-bg);
  font-family: 'VT323', monospace;
  font-size: 0.8rem;
  line-height: 1;
}

.ql-checkbox input:checked + .ql-checkbox-box {
  background: var(--ql-accent);
}

.ql-checkbox input:checked + .ql-checkbox-box::after {
  content: 'X';
}

.ql-select {
  align-self: stretch;
  display: block;
  width: 100%;
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  padding: 0.4rem 0.5rem;
  margin-bottom: 1rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
}

.ql-meal-results {
  align-self: stretch;
  list-style: none;
  margin: 1rem 0;
  padding: 0;
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--ql-accent);
}

.ql-meal-results:empty {
  border: none;
  margin: 0;
}

.ql-meal-result {
  border-bottom: 1px dotted rgba(57, 255, 138, 0.3);
}

.ql-meal-result:last-child {
  border-bottom: none;
}

.ql-meal-result label {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.5rem;
  cursor: pointer;
}

.ql-meal-result label:hover {
  background: rgba(57, 255, 138, 0.15);
}

.ql-meal-result input[type='radio'] {
  accent-color: var(--ql-accent);
  margin-top: 0.3rem;
  cursor: pointer;
}

.ql-meal-result__name {
  display: block;
}

.ql-meal-result__description {
  display: block;
  font-size: 0.9rem;
  color: var(--ql-muted);
}

/* Serving size + quantity picker, shown once a search result is selected.
   Same "multiple fields in a row" layout as .ql-history-controls. A class
   selector, not #ql-meal-portion — an ID selector here would outrank
   .ql-hidden's plain class selector regardless of source order, so the
   `display: none` toggle would never actually win (real bug, caught before
   shipping; see MEMORY.md's CSS-specificity note). */
.ql-meal-portion {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
  margin-bottom: 1rem;
}

/* Combined selector so this always wins over .ql-meal-portion's own
   `display: flex`, regardless of which rule appears later in the file —
   same equal-specificity/source-order trap as .ql-modal-overlay.ql-hidden
   above (see the general-purpose .ql-hidden comment near the top of this
   file): two equal-specificity rules on the same element would otherwise
   let source order silently decide, and .ql-meal-portion is declared after
   .ql-hidden. Real second-round bug: switching #ql-meal-portion to a class
   selector fixed the ID-vs-class problem but not this one — the class
   selector alone still ties with .ql-hidden and loses on source order. */
.ql-meal-portion.ql-hidden {
  display: none;
}

/* Daily Records / Monthly Review — box removed 2026-07-22 (matching
   Profile/Boss Battle's unboxed treatment); separation from Today's Quest
   above is now carried by .ql-history-divider (below) instead of a
   border/panel fill. Monthly Review (added 2026-07-10) still lives inside
   this same block as a second .ql-records-subsection. */
.ql-history {
  text-align: left;
  width: 90%;
  max-width: 1320px;
  margin: 1.5rem auto 0;
  padding: 0 1.75rem 1.5rem;
}

/* Divider replacing .ql-history's old box top edge — panel-width (not
   full-bleed, reverted 2026-07-22 per direct request), matching .ql-history
   itself. The ::after glow dips briefly *lighter* (a thin accent-green
   wash) before falling through a dark band to fully transparent: a plain
   darker shadow here is invisible since the page background (#04120b) is
   already near-black and has no room to get darker (verified via a
   side-by-side render against a mid-gray swatch, mockup-approved
   2026-07-22). */
.ql-history-divider {
  position: relative;
  width: 90%;
  max-width: 1320px;
  margin: 2rem auto 0;
  border-top: 1px solid rgba(57, 255, 138, 0.35);
}

.ql-history-divider::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 70px;
  background: linear-gradient(
    180deg,
    rgba(57, 255, 138, 0.16) 0%,
    rgba(0, 0, 0, 0.55) 45%,
    rgba(0, 0, 0, 0) 100%
  );
  pointer-events: none;
}

/* The divider above already marks where this section starts, so Daily
   Records' own heading skips the usual .ql-manual-heading::after top rule
   (would otherwise double the line right under the glow). Monthly Review's
   heading keeps it as-is — no change to the divider between the two
   subsections. */
#ql-history-heading.ql-manual-heading::after {
  display: none;
}

/* Daily Records and Monthly Review titles use .ql-manual-heading (below),
   which supplies its own divider + top spacing (padding-top: 2rem) as part
   of the component — no extra subsection-level divider/margin needed here
   (revised 2026-07-10: this used to carry its own border-top + margin
   before both titles switched to .ql-manual-heading, which would have
   doubled the line above Monthly Review). */
.ql-records-subsection + .ql-records-subsection {
  margin-top: 0;
}

/* How This Works — plain text, no box. Same width/alignment as the rest
   of the page so it still lines up, but no border/panel fill/glow.
   Reverted 2026-07-10 back to this origin terminal style after a brief
   "Quest Briefing" poster departure read as disharmonious with the rest
   of the page. */
/* Lifted above the whole-viewport CRT scanline (body::before, z-index 999,
   see Elevation) via its own stacking context — the scanline's horizontal
   lines were making this section's dense body text hard to read. Scoped to
   just this section (not a page-wide opt-out like Data Export/Settings/
   Manual use) since the game panels above it and Contact below it keep the
   scanline unchanged. Real bug, fixed same day: this z-index also used to
   beat .ql-topbar's, so once this section scrolled up under the fixed nav
   its text painted through it instead of underneath — .ql-topbar is now
   1001, above this. */
#ql-how-it-works {
  position: relative;
  z-index: 1000;
  text-align: left;
  width: 90%;
  max-width: 1320px;
  margin: 2rem auto 0;
  padding: 0 1.75rem;
}

/* Contact — solid Terminal Green panel (revised 2026-07-10), reverse-video
   at the section scale: the same "fill flips to the accent color, text
   flips to Void Black" language as buttons/Daily-Records-row hover, just
   applied to a whole section instead of a hover state. Still the same
   width/alignment as the rest of the page (not full-bleed), just filled
   instead of transparent. */
/* Option 3 ("title-only stamp", direct request 2026-07-22, replacing the
   full solid-fill panel): the new brighter accent (#39ff8a) made a
   full-bleed fill read as staring into a light source. Only the title
   itself now sits on a small solid-fill stamp (.ql-contact-row__title-
   stamp below); the section itself is lifted the same way above the
   whole-page CRT scanline (position: relative; z-index: 1000 — see
   body::before's own comment) since its text would otherwise be no more
   readable through the scanline than How This Works' was.

   Same-day follow-up: full-bleed background + divider (direct request,
   mockup-approved). #ql-contact itself now spans the true viewport width
   via the standard "breakout" margin trick (`margin-left`/`margin-right:
   calc(50% - 50vw)` + `width: 100vw`) — the classic way to escape a
   centered parent's own max-width without restructuring the whole page's
   markup. This exact formula cancels out to 0 regardless of the parent's
   own width, as long as the parent is itself horizontally centered in the
   viewport (`.ql-panel--wide` is, via body's flex centering) — verified
   live via Playwright (`getBoundingClientRect()` reports x:0, width equal
   to the viewport). A `left`/`right: 50%` + negative-margin variant was
   tried first and measured incorrectly offset — percentages there
   resolve against the *containing block* (the parent, not the viewport),
   which doesn't reliably cancel the same way this margin-only formula
   does. The actual readable content (.ql-contact-row, below) gets its
   own width: 90%/max-width: 1320px/margin: auto instead, so it still
   lines up with How This Works and every other section despite the
   outer element being full width. border-top doubles as both the new
   divider from How This Works and the section's own top edge —
   full-bleed since the whole element is. Background is the same
   green→black gradient already used on the Choose Your Character panel,
   layered with the Obstacle guidance pop-up's grid-texture + two-layer
   inward-glow technique (.ql-hud-modal, see its own comment) — reused
   values, not new ones. See the html/body overflow-x note below for why
   100vw is safe here.

   Real bug caught live (user report: "you forgot the grid background"):
   multiple `background-image` layers paint with the *first* listed on
   top, not the last — the opaque 100%-sized gradient was listed first,
   completely hiding the two grid-line layers behind it, even though all
   3 layers were genuinely present in the computed style. Fixed by
   reordering: the grid lines (mostly-transparent) go first so they paint
   over the gradient, and the solid gradient goes last as the base fill.
   Verified via a boosted-opacity Playwright test that the grid was
   invisible even at 0.9 alpha before the reorder, and clearly visible
   after it at the real 0.1 alpha. */
#ql-contact {
  position: relative;
  width: 100vw;
  /* Real gap above the divider — the previous non-full-bleed rule had
     `margin: 2rem auto 0`, but converting to the margin-left/right
     breakout above (direct request 2026-07-22) dropped the vertical
     margin entirely, leaving almost no space between this section and
     How This Works' last paragraph. Bumped to 3rem (from the old 2rem)
     per direct follow-up request for more breathing room, on top of
     the divider itself. */
  margin-top: 3rem;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  z-index: 1000;
  text-align: left;
  padding: 2.5rem 0;
  border-top: 1px solid rgba(57, 255, 138, 0.35);
  background-color: #030d08;
  background-image:
    linear-gradient(rgba(57, 255, 138, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(57, 255, 138, 0.1) 1px, transparent 1px),
    linear-gradient(180deg, #0c2e1d 0%, #02100a 100%);
  background-size:
    12px 12px,
    12px 12px,
    100% 100%;
  box-shadow:
    inset 0 0 70px rgba(57, 255, 138, 0.25),
    inset 0 0 120px rgba(0, 0, 0, 0.75);
}

/* Contact row layout, revised 2026-07-10 — title and body side by side
   instead of stacked, matching How This Works' "adjust the layout, keep
   the terminal style" treatment. No divider/prompt line here (unlike
   How This Works/Daily Records/Monthly Review's .ql-manual-heading) —
   the reference layout for this one is a clean two-up row, title as the
   visual anchor rather than a "section start" marker. Flexbox with
   space-between (revised again 2026-07-11, was a grid `auto 1fr` column
   — that left the body text floating in a wide column with a lot of dead
   green space after it) so the body block sits flush against the right
   edge instead of just starting somewhere left-of-center. */
.ql-contact-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 3.5rem;
  width: 90%;
  max-width: 1320px;
  margin: 0 auto;
  padding: 0 1.75rem;
}

/* Groups the title + intro paragraph as one left-hand block (direct
   request 2026-07-21: the paragraph used to live in the right-hand
   .ql-contact-row__body, next to/above the form — moved here so it reads
   as "Contact" followed by its own intro line, with the form as a fully
   separate block on the right). */
.ql-contact-row__intro {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: 0.85rem;
}

/* Unstyled, this link renders as the browser default blue — jarring
   against the terminal-green theme. Matches the site's accent color. */
.ql-contact-row__email-link {
  color: var(--ql-accent);
}

.ql-contact-row__email-link:visited {
  color: var(--ql-accent);
}

/* Grows to fill the row's remaining width (rather than shrink-wrapping
   the form's own content) so .ql-contact-form's max-width actually has
   room to be used — direct request 2026-07-21 to widen the form. */
.ql-contact-row__body {
  flex: 1;
}

/* Small solid-fill "stamp" behind just the title (Option 3, replacing the
   full-panel fill — see #ql-contact's own comment) — keeps a genuine
   reverse-video accent moment without a large area of the bright accent
   color. inline-block so the stamp hugs the title's own text instead of
   spanning the intro column's full width. */
.ql-contact-row__title-stamp {
  display: inline-block;
  background: var(--ql-accent);
  padding: 0.3rem 1rem;
}

/* Bigger (direct request 2026-07-22, after an earlier squeezed/condensed
   version was tried and rejected) — plain proportions otherwise, no
   transform/letter-spacing change. Dark ink stays correct here regardless
   of the section's own background, since the title now always sits on
   its own accent-filled stamp above. */
.ql-contact-row__title {
  font-family: 'VT323', monospace;
  font-size: 4.5rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ql-bg);
  margin: 0;
}

/* Text color flipped from dark-on-green to the standard light-on-dark
   palette (Option 3 — the section is a normal dark background now, not a
   solid green fill). */
.ql-contact-row__intro p {
  font-size: 1.3rem;
  line-height: 1.6;
  color: var(--ql-text);
  margin: 0;
  max-width: 46ch;
}

/* Contact form — mockup "Variant E" (approved 2026-07-21): a plain ruled
   list, no boxed inputs, each field labeled as its own command-line prompt
   (NAME>/EMAIL>/MESSAGE>) rather than a plain word — matches How This
   Works' own "plain text, no box" philosophy instead of introducing a
   boxed-input surface. Matches How This Works' right-column paragraph
   width (~516px / 32rem at a 1440px viewport, measured directly) rather
   than an arbitrary ch value. (A corner-bracket frame around this was
   tried and removed per follow-up request — back to no extra wrapper.)
   Colors flipped to the standard light-on-dark palette (Option 3 — the
   section is a normal dark background now, not a solid green fill; see
   #ql-contact's own comment) — this and #ql-contact share the same
   z-index lift, so a second one here would be redundant, not removed for
   safety in case this component is ever reused somewhere without that
   ancestor lift. */
.ql-contact-form {
  position: relative;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  max-width: 32rem;
  margin-left: auto;
}

.ql-contact-form__field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid rgba(57, 255, 138, 0.25);
}

.ql-contact-form__field:has(textarea) {
  align-items: flex-start;
}

.ql-contact-form__field label {
  flex-shrink: 0;
  width: 7rem;
  font-family: 'VT323', monospace;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ql-muted);
}

.ql-contact-form__field input,
.ql-contact-form__field textarea {
  flex: 1;
  min-width: 0;
  font-family: 'VT323', monospace;
  font-size: 1.2rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--ql-text);
  padding: 0;
  text-align: right;
}

.ql-contact-form__field textarea {
  text-align: left;
  resize: vertical;
  min-height: 3.5rem;
  padding-top: 0.15rem;
}

.ql-contact-form__field input::placeholder,
.ql-contact-form__field textarea::placeholder {
  color: rgba(216, 245, 232, 0.35);
}

.ql-contact-form__field input:focus-visible,
.ql-contact-form__field textarea:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

/* Reuses .ql-button's bracket-content/reverse-video language directly —
   Terminal Green text at rest, fills to Terminal Green with Void Black
   text on hover, the same as every other button on the site now that
   this section's own background is dark rather than a green fill. */
.ql-contact-form__submit {
  align-self: flex-start;
  margin-top: 1.1rem;
  display: inline-block;
  font-family: 'VT323', monospace;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: transparent;
  color: var(--ql-accent);
  text-shadow: 0 0 2px rgba(57, 255, 138, 0.6);
  border: none;
  border-radius: 0;
  padding: 0.5rem 0;
  cursor: pointer;
}

.ql-contact-form__submit::before {
  content: '[ ';
}

.ql-contact-form__submit::after {
  content: ' ]';
}

.ql-contact-form__submit:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
  text-shadow: none;
}

.ql-contact-form__submit:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-contact-form__submit:disabled {
  color: var(--ql-muted);
  text-shadow: none;
  cursor: not-allowed;
}

.ql-contact-form__submit:disabled:hover {
  background: transparent;
  color: var(--ql-muted);
}

/* How This Works' title, bigger than the standard .ql-manual-heading size
   used by Contact/Daily Records/Monthly Review — this is the page's most
   important explainer, so it gets more weight than the others. */
#ql-how-it-works-heading.ql-manual-heading {
  font-size: 2.6rem;
}

/* Two-column layout kept from the "Quest Briefing" exploration (pull-quote
   + supporting text on the left, a paragraph stack with inline-highlighted
   phrases on the right) but recolored into the terminal system: Terminal
   Green instead of coral, VT323 (inherited from body) instead of a system
   sans, reverse-video highlight (Terminal Green fill / Void Black text)
   matching the same emphasis language as buttons and Daily Records rows,
   rather than a one-off accent color. */
.ql-how-it-works-grid {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 3rem;
  margin-top: 1.5rem;
}

.ql-how-it-works__pull {
  font-size: 1.8rem;
  line-height: 1.3;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.5);
  margin: 0 0 1.2rem;
}

.ql-how-it-works__support {
  font-size: 1.3rem;
  line-height: 1.6;
  color: var(--ql-text);
  margin: 0 0 1rem;
  max-width: 52ch;
}

.ql-how-it-works__stack p {
  font-size: 1.3rem;
  line-height: 1.6;
  color: var(--ql-text);
  margin: 0 0 1.1rem;
}

.ql-how-it-works__highlight {
  color: var(--ql-bg);
  background: var(--ql-accent);
  font-weight: 700;
  padding: 0 0.25rem;
  box-decoration-break: clone;
}

.ql-how-it-works__header {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 1.5rem;
}

.ql-how-it-works__manual-link {
  flex-shrink: 0;
  margin-bottom: 0.75rem;
  font-size: 1.2rem;
}

/* "Manual" title format: a plain divider rule marking the start of the
   section, a dim `$ man ...` shell-prompt line, then the heading itself —
   no `>` prefix here (unlike .ql-section-heading elsewhere), since the
   prompt line above it already carries that "command" read. The divider
   is absolutely positioned (top:0 of the heading's own padding box) so it
   can render visually above the ::before prompt line despite ::after
   normally painting after an element's real content. */
.ql-manual-heading {
  position: relative;
  padding-top: 2rem;
  margin: 0 0 0.75rem;
  font-family: 'VT323', monospace;
  font-size: 1.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.6);
}

.ql-manual-heading::before {
  content: '$ man blip-up/how-this-works';
  display: block;
  margin: 0 0 0.5rem;
  font-size: 1rem;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ql-muted);
  text-shadow: none;
}

#ql-history-heading.ql-manual-heading::before {
  content: '$ man blip-up/daily-records';
}

#ql-monthly-review-heading.ql-manual-heading::before {
  content: '$ man blip-up/monthly-review';
}

.ql-manual-heading::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  border-top: 1px solid rgba(57, 255, 138, 0.35);
}

/* Date picker + type filter side by side, same row layout the Data Export
   page already uses for its From/To/Daily-Monthly controls (.ql-trend-
   controls/.ql-field--inline below) — reused here instead of inventing a
   second pattern for the same "multiple fields in a row" need. */
.ql-history-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1.5rem;
  margin-bottom: 1rem;
}

.ql-history-controls .ql-field--inline {
  margin-bottom: 0;
}

/* Resets the browser's default fieldset border/padding so it reads as a
   plain .ql-field wrapper — <fieldset>/<legend> instead of <label for>
   here because the filter is a group of three checkboxes, not one input. */
.ql-history-filter {
  min-width: 0;
  border: 0;
  padding: 0;
}

.ql-history-filter legend {
  padding: 0;
  margin-bottom: 0.25rem;
  font-size: 1.05rem;
  color: var(--ql-muted);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* Row instead of .ql-checkbox-group's default vertical stack (that default
   suits the Power-Ups form, a single decision list; this is three short
   toggles meant to sit compactly next to the Date field). */
.ql-checkbox-group--inline {
  flex-direction: row;
  flex-wrap: wrap;
  column-gap: 1rem;
  margin-bottom: 0;
}

.ql-history input[type='date'] {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
}

/* Chrome's native calendar icon is drawn dark, meant for light input
   backgrounds — invisible against this now-dark field without inverting. */
.ql-history input[type='date']::-webkit-calendar-picker-indicator {
  filter: invert(1);
}

.ql-history-list {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--ql-accent);
}

.ql-history-list:empty {
  border: none;
  margin: 0;
}

/* Reverse-video row hover, matching the button/checkbox language — a real
   TUI file-list selection, not a border/underline highlight. */
.ql-history-item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.3);
}

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

.ql-history-item:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

.ql-history-item:hover .ql-history-item__type,
.ql-history-item:hover .ql-history-item__points,
.ql-history-item:hover .ql-history-item__time,
.ql-history-item:hover .ql-history-item__portion,
.ql-history-item:hover .ql-history-item__delete {
  color: var(--ql-bg);
}

.ql-history-item__type {
  display: block;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
}

.ql-history-item__detail {
  display: block;
}

/* Portion/serving size (e.g. "2 x 1 cup") — only meals ever have one, see
   intake-routes.js's formatPortion. Muted like .ql-history-item__time
   below, but sized up from an initial 0.85rem (real feedback: VT323 at that
   size read as too hard to read here) — still visually secondary via color
   alone, not by being tiny. */
.ql-history-item__portion {
  display: block;
  font-size: 1.05rem;
  color: var(--ql-muted);
}

.ql-history-item__meta {
  text-align: right;
  flex-shrink: 0;
}

.ql-history-item__points {
  display: block;
  color: var(--ql-accent);
}

.ql-history-item__time {
  display: block;
  font-size: 0.9rem;
  color: var(--ql-muted);
}

/* Undo-a-mis-click affordance (2026-07-20) — muted/underlined at rest so it
   doesn't compete with the actual record, matching .ql-history-item__time's
   quiet treatment, but still inverts with the rest of the row on hover (see
   .ql-history-item:hover above) so it's easy to find on the row you're
   actually looking at. */
.ql-history-item__delete {
  display: block;
  /* Buttons don't stretch to fill a block container's width the way divs/
     spans do (a form-control sizing quirk — width:auto resolves to the
     button's own content size even under display:block), so .ql-history-
     item__meta's text-align:right never reached it: the box sat flush
     left, tight to "Delete"'s own text, misaligned under the right-
     aligned time/points above it. margin-left:auto pushes that
     intrinsically-sized box to the column's right edge instead. */
  margin-top: 0.2rem;
  margin-left: auto;
  background: none;
  border: none;
  padding: 0;
  font-family: 'VT323', monospace;
  font-size: 0.9rem;
  color: var(--ql-muted);
  text-decoration: underline;
  cursor: pointer;
}

.ql-history-item__delete:hover,
.ql-history-item__delete:focus-visible {
  text-decoration: none;
}

/* Monthly Review reuses the .ql-history panel/box styling as-is (same
   double-border frame, same spacing) — only the month input and table
   below are new. */
.ql-history input[type='month'] {
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
}

.ql-history input[type='month']::-webkit-calendar-picker-indicator {
  filter: invert(1);
}

.ql-monthly-review-table {
  width: 100%;
  margin: 1rem 0 0;
  border-collapse: collapse;
}

.ql-monthly-review-table th,
.ql-monthly-review-table td {
  padding: 0.5rem 0.75rem;
  text-align: left;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.3);
}

.ql-monthly-review-table th {
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  border-bottom: 1px solid var(--ql-accent);
}

/* Stat columns line up on their digits, matching the stat bars' own
   numeric display elsewhere on the page. */
.ql-monthly-review-table td:not(:first-child) {
  font-variant-numeric: tabular-nums;
}

.ql-monthly-review-table tbody tr:last-child td {
  border-bottom: none;
}

/* Same reverse-video row hover as .ql-history-item, for one consistent
   "selectable row" language across both tables. */
.ql-monthly-review-table tbody tr:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

/* Totals row — set apart from the daily rows with a solid top border (vs.
   the dotted row separators above) and Terminal Green bold text, the same
   "this number matters more" language as the stat bar values. */
.ql-monthly-review-table tfoot th,
.ql-monthly-review-table tfoot td {
  border-top: 2px solid var(--ql-accent);
  border-bottom: none;
  font-weight: 700;
  color: var(--ql-accent);
  text-shadow: 0 0 4px rgba(57, 255, 138, 0.5);
}

.ql-monthly-review-table tfoot th {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.85rem;
}

/* Table (+ totals) on the left, trend chart on the right — same two-column
   console layout as the main Today's Quest/Log split (.ql-layout), reusing
   flexbox instead of grid so the table column naturally reclaims the full
   width when the chart column is hidden (empty month, no data to plot). */
.ql-monthly-review-layout {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.75rem;
  margin-top: 1rem;
}

.ql-monthly-review-table-col {
  flex: 1 1 380px;
  min-width: 0;
}

.ql-monthly-review-chart-col {
  flex: 1 1 320px;
  min-width: 0;
  padding-left: 1.75rem;
  border-left: 1px solid rgba(57, 255, 138, 0.35);
}

.ql-monthly-review-table-col .ql-monthly-review-table {
  margin-top: 0;
}

/* Positioning context for the HTML tooltip overlay, which sits as a
   sibling of the <svg> rather than inside it (SVG can't host arbitrary
   HTML/CSS-styled boxes the way a plain div can). */
.ql-monthly-review-chart-wrap {
  position: relative;
}

.ql-monthly-review-chart {
  display: block;
  width: 100%;
  height: auto;
}

.ql-monthly-review-chart .ql-chart-hit {
  fill: transparent;
  cursor: pointer;
}

/* Lines/dots never intercept the mouse, so hovering exactly on a marker or
   a stroked line segment still falls through to the day-band .ql-chart-hit
   rect underneath (see app.js) — otherwise a solid-painted dot/line would
   grab the pointer event and the tooltip would drop out right where you'd
   expect it to be most precise. */
.ql-monthly-review-chart .ql-chart-line,
.ql-monthly-review-chart .ql-chart-dot {
  pointer-events: none;
}

/* Follows the cursor over a day's column, showing that day's date plus
   whichever stat(s) are closest to the cursor's height (app.js groups by
   actual value, so more than one stat only ever appears together when
   they truly share a value/y position that day). No border — still
   unboxed in spirit — but a mostly-opaque backing wash keeps the
   left-aligned text legible over the chart's own lines/gridlines. */
.ql-chart-tooltip {
  position: absolute;
  z-index: 10;
  transform: translate(-50%, calc(-100% - 10px));
  padding: 0.35rem 0.6rem;
  background: rgba(4, 18, 11, 0.82);
  text-align: left;
  font-family: 'VT323', monospace;
  font-size: 1.05rem;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
}

.ql-chart-tooltip__date {
  color: var(--ql-muted);
  font-size: 0.9rem;
  margin-bottom: 0.1rem;
}

/* One shared color for all 3 stats here — the tooltip only ever shows
   more than one row when they share a value, so per-stat colors aren't
   needed to tell rows apart the way the chart lines/legend still do. */
.ql-chart-tooltip__row {
  color: var(--ql-text);
}

.ql-monthly-review-chart .ql-chart-grid {
  stroke: rgba(57, 255, 138, 0.25);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

.ql-monthly-review-chart .ql-chart-axis {
  stroke: rgba(57, 255, 138, 0.5);
  stroke-width: 1;
}

.ql-monthly-review-chart .ql-chart-axis-label {
  font-family: 'VT323', monospace;
  font-size: 11px;
  fill: var(--ql-muted);
}

.ql-monthly-review-chart .ql-chart-line {
  fill: none;
  stroke-width: 1.5;
}

.ql-monthly-review-chart .ql-chart-line--stamina {
  stroke: var(--ql-accent);
}

.ql-monthly-review-chart .ql-chart-line--focus {
  stroke: var(--ql-accent-cyan);
}

.ql-monthly-review-chart .ql-chart-line--health {
  stroke: #f2c14e;
}

.ql-monthly-review-chart .ql-chart-dot {
  shape-rendering: crispEdges;
}

.ql-monthly-review-chart .ql-chart-dot--stamina {
  fill: var(--ql-accent);
}

.ql-monthly-review-chart .ql-chart-dot--focus {
  fill: var(--ql-accent-cyan);
}

.ql-monthly-review-chart .ql-chart-dot--health {
  fill: #f2c14e;
}

.ql-monthly-review-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  font-size: 0.9rem;
}

.ql-monthly-review-legend__item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.ql-monthly-review-legend__item::before {
  content: '';
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
}

.ql-monthly-review-legend__item--stamina::before {
  background: var(--ql-accent);
}

.ql-monthly-review-legend__item--focus::before {
  background: var(--ql-accent-cyan);
}

.ql-monthly-review-legend__item--health::before {
  background: #f2c14e;
}

@media (max-width: 900px) {
  .ql-monthly-review-chart-col {
    padding-left: 0;
    border-left: none;
    flex-basis: 100%;
  }
}

/* Real bug, not just a size tweak: .ql-page is a flex item of `body`
   (align-items: center), so with no explicit width it shrink-wraps to
   its own content's fit-content size instead of filling the viewport.
   .ql-plain-page's `width: 90%` below then resolves against THAT
   shrunk, content-dependent size rather than the page's real available
   width — a fragile pattern .ql-panel/.ql-history also technically rely
   on, but their wide titlebar/nav content happened to keep the
   equilibrium width large enough to look correct. Removing the
   frame/titlebar/nav (this page's own "unframed" revision, same date)
   left only narrower content, so the same 90% rule silently resolved to
   a visibly smaller page. Fix: give .ql-page a definite width via
   `align-self: stretch` (scoped to this page only, via the body class
   already used for the scanline opt-out — every other page keeps
   .ql-page's default shrink-wrap, which their narrower .ql-panel boxes
   depend on for centering). */
body.ql-no-page-scanline .ql-page {
  align-self: stretch;
}

/* Same shrink-wrap bug as above, now hit on Profile/Boss Battle too
   (2026-07-22): combining the titlebar and nav into one row needs more
   width than the old stacked layout did, and these two pages' own content
   (badge grids, attack/defence text) is narrower than index.html's
   3-column log grid — not enough to keep the equilibrium width above the
   combined header's needs, so the nav row wrapped even at desktop widths.
   Same fix, same reasoning, just a different opt-out class. */
body.ql-unboxed .ql-page {
  align-self: stretch;
}

/* Data Export page (revised 2026-07-12): a staff-facing data/reporting
   tool, not a "terminal window" — unframed (no .ql-panel double border,
   no titlebar, no [ Today's Quest ] nav row), plain left-aligned text
   throughout instead of .ql-panel's centered default. Sized the same as
   .ql-panel--wide (90% / max-width 1320px) so it still lines up with the
   rest of the site, just without the frame. */
.ql-plain-page {
  width: 80%;
  max-width: 1320px;
  margin: 1.5rem auto 2rem;
  text-align: left;
}

.ql-heading--left {
  text-align: left;
}

.ql-plain-section {
  margin-top: 2.5rem;
}

.ql-plain-section:first-of-type {
  margin-top: 1.5rem;
}

/* Section title without the "$ man ..." shell-prompt line .ql-manual-heading
   uses elsewhere — that's exactly the "cryptic terminal chrome" this page
   is dropping for a non-technical staff audience. Keeps the same weight
   (VT323 + glow) and a plain top divider for scannability between
   sections, just without the prompt text above it. */
.ql-section-title {
  position: relative;
  padding-top: 1.5rem;
  margin: 0 0 0.75rem;
  font-family: 'VT323', monospace;
  font-size: 1.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.6);
}

.ql-section-title::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  border-top: 1px solid rgba(57, 255, 138, 0.35);
}

/* Account page's Delete My Data danger zone (direct request, 2026-08-01,
   referencing GitHub's own delete-account pattern) — the one deliberate
   exception to this product's alarm-free palette, reserved for exactly one
   irreversible action. A bordered box (not just color) sets it apart from
   the safe Export section above it, same "framed surface" principle as
   every other bordered box in this system, just in --ql-danger instead of
   Terminal Green. */
.ql-danger-zone {
  border: 1px solid var(--ql-danger);
  padding: 1.25rem 1.5rem 1.5rem;
}

.ql-section-title--danger {
  color: var(--ql-danger);
  text-shadow:
    0 0 2px var(--ql-danger),
    0 0 8px rgba(255, 59, 59, 0.6);
}

.ql-section-title--danger::before {
  border-top-color: rgba(255, 59, 59, 0.35);
}

.ql-danger-text {
  color: var(--ql-danger);
  font-size: 1.05rem;
  margin: 0 0 1rem;
}

.ql-button--danger {
  color: var(--ql-danger);
  text-shadow: 0 0 2px rgba(255, 59, 59, 0.6);
}

.ql-button--danger:hover {
  background: var(--ql-danger);
  color: var(--ql-bg);
  text-shadow: none;
}

/* Data Export page: Historical Trend controls + Activity Breakdown bars.
   The trend chart itself reuses .ql-monthly-review-chart's CSS scope
   directly (see data-export.html) rather than duplicating ~80 lines of
   identical axis/line/dot rules for a second chart that looks the same. */
.ql-trend-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1.5rem;
  margin-bottom: 1rem;
}

/* Pushes just the Export button flush to the row's right edge — the
   classic flex "auto margin" trick — while From/To/Daily-Monthly stay
   grouped together on the left, unlike `justify-content: space-between`
   which would spread every item apart evenly. */
#ql-export-button {
  margin-left: auto;
}

/* min-width:0 lets this shrink below its content's natural size — without
   it, a flex item (this is one, inside .ql-meal-portion/.ql-trend-controls)
   refuses to shrink narrower than its widest child by default, regardless
   of the child's own max-width. */
.ql-field--inline {
  margin-bottom: 0;
  min-width: 0;
}

.ql-field--inline .ql-select,
.ql-field--inline input {
  width: auto;
  min-width: 10rem;
  max-width: 100%;
  margin-bottom: 0;
}

/* Real fix for the Serving Size overflow (2026-08-02, second attempt — the
   first, `max-width: 100%` above, didn't actually take effect and here's
   why): a `<select>`'s native closed-box width is based on its *widest*
   option in the whole list, not just the one currently shown — so even a
   short-looking selection like "100 g (26 kcal)" can render wide if some
   other option in that food's serving list is long. That alone wouldn't be
   a layout bug if the column could clip it, but `max-width: 100%` on the
   select can only resolve against a *definite* width on its containing
   block — `.ql-field--inline`, a flex item with no `flex-basis`, has none;
   its own box is still sized by its child's content (shrink-to-fit), so
   the percentage had nothing real to resolve against and was effectively
   ignored. Giving each field a real flex-basis breaks that circularity:
   now the select's 100% has something concrete to be 100% *of*, so it
   actually clips regardless of the widest option's text length. Scoped to
   `.ql-meal-portion` specifically (not the shared base rule above) since
   Data Export's/Daily Records' own `.ql-field--inline` usages size
   correctly already and don't need this. */
.ql-meal-portion .ql-field--inline {
  flex: 1 1 12rem;
  min-width: 0;
  position: relative;
}

.ql-meal-portion .ql-field--inline .ql-select,
.ql-meal-portion .ql-field--inline input {
  width: 100%;
  max-width: 100%;
}

/* A long serving description still can't fit a native <select>'s
   single-line closed box (browsers won't wrap it, no CSS override exists)
   — this at least trims it to a clean "…" instead of an abrupt mid-word
   cutoff. Support varies by browser/OS; where it's not honored, the result
   is the same plain truncation as before, never worse. The full text is
   still available via the custom tooltip below (app.js's
   showMealServingTooltip) — a `title`-attribute attempt was tried first
   but confirmed live to not work (Chrome doesn't show it on a <select>'s
   closed box). */
.ql-meal-portion .ql-field--inline .ql-select {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Custom tooltip (app.js's showMealServingTooltip/
   hideMealServingTooltip) — replaces an unreliable `title`-attribute
   approach (confirmed live: Chrome doesn't show it on a <select>'s closed
   box). Anchored to `.ql-field--inline`'s `position: relative` above,
   sitting just below the select; wraps normally (unlike the select itself)
   since it's a plain span, not a native form control. */
.ql-select-tooltip {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  margin-top: 0.35rem;
  padding: 0.5rem 0.75rem;
  max-width: 20rem;
  background: var(--ql-bg);
  border: 1px solid var(--ql-accent);
  color: var(--ql-text);
  font-size: 0.95rem;
  white-space: normal;
}

.ql-select-tooltip.ql-hidden {
  display: none;
}

/* Chrome's native calendar icon is drawn dark, meant for light input
   backgrounds — invisible against this dark field without inverting
   (same fix Daily Records/Monthly Review already have, scoped to
   .ql-history — this page isn't inside that container, so it needs its
   own copy). Without this, the picker button is effectively invisible,
   which reads as "you can only type the date." */
.ql-plain-page input[type='date']::-webkit-calendar-picker-indicator,
.ql-plain-page input[type='month']::-webkit-calendar-picker-indicator {
  filter: invert(1);
}

/* Caps the Historical Trend chart's width so it doesn't stretch to fill
   the full section — scoped to this page's own chart by id, not the
   shared .ql-monthly-review-chart-wrap class, so Monthly Review's chart
   on index.html is untouched. */
#ql-trend-chart-wrap {
  max-width: 640px;
}

.ql-toggle-group {
  display: flex;
  border: 1px solid var(--ql-accent);
}

.ql-toggle-button {
  font-family: 'VT323', monospace;
  font-size: 1.05rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.45rem 1rem;
  background: transparent;
  color: var(--ql-text);
  border: none;
  cursor: pointer;
}

.ql-toggle-button + .ql-toggle-button {
  border-left: 1px solid var(--ql-accent);
}

.ql-toggle-button:hover {
  color: var(--ql-accent);
}

.ql-toggle-button--active,
.ql-toggle-button--active:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

/* Fixed 2 columns, not auto-fit — this app is desktop-only (no mobile
   reflow, see AGENTS.md/MEMORY.md), and a fixed column count is also
   what guarantees Obstacles Logged (the 3rd item, auto-placed onto its
   own row by the grid) ends up exactly as wide as Meals Logged above it:
   one grid keeps its column tracks identical across every row it lays
   out, which separate grid containers per row couldn't guarantee. */
.ql-breakdown-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(240px, 1fr));
  gap: 1.75rem;
  margin-top: 0.5rem;
}

.ql-breakdown-group__heading {
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ql-muted);
  margin: 0 0 0.6rem;
}

.ql-breakdown-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.ql-breakdown-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 2fr auto;
  align-items: center;
  gap: 0.6rem;
}

.ql-breakdown-item__label {
  font-size: 1.15rem;
  color: var(--ql-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ql-breakdown-item__track {
  display: block;
  height: 0.7rem;
  background: var(--ql-bg);
  border: 1px solid rgba(57, 255, 138, 0.35);
}

.ql-breakdown-item__fill {
  display: block;
  height: 100%;
  background: var(--ql-accent);
}

.ql-breakdown-item__value {
  font-variant-numeric: tabular-nums;
  font-size: 1.1rem;
  color: var(--ql-muted);
  min-width: 1.5ch;
  text-align: right;
}

/* Meals Logged rows are clickable (Power-Ups/Obstacles aren't — there's
   no finer detail to drill into for those) — expands to the specific
   foods logged under that meal type. Kept as a plain clickable <li>
   (role="button", not a real <button>) so it stays a grid item lined up
   with the label/track/value columns above it without an extra wrapping
   box breaking the grid; keyboard support (Enter/Space) added in JS. */
.ql-breakdown-item--expandable {
  cursor: pointer;
}

.ql-breakdown-item--expandable:hover .ql-breakdown-item__label {
  color: var(--ql-accent);
}

.ql-breakdown-item__caret {
  display: inline-block;
  width: 1ch;
  margin-right: 0.3rem;
  color: var(--ql-muted);
}

.ql-breakdown-item__detail {
  grid-column: 1 / -1;
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0.5rem 0 0 1.6rem;
  border-top: 1px dotted rgba(57, 255, 138, 0.3);
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 1rem;
  color: var(--ql-text);
}

.ql-breakdown-item__detail li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

/* Same fix as .ql-modal-overlay.ql-hidden (see the comment on .ql-hidden
   at the top of this file): .ql-breakdown-item__detail's own
   `display: flex` ties with .ql-hidden's `display: none` at equal
   specificity, and would win on source order alone since it's declared
   later in the file. This compound selector wins regardless of order. */
.ql-breakdown-item__detail.ql-hidden {
  display: none;
}

/* Admin page (2026-08-02): tightens the gap between the "Admin" H1 and the
   Client Roster section's top divider — .ql-error reserves margin-top +
   min-height even while empty (real content on every other page that
   reuses it, e.g. Settings/Data Export, so left alone there), but on this
   page it's rarely populated and the resulting empty gap read as too much
   dead space right under the heading. */
body.ql-admin-page #ql-error {
  margin-top: 0;
  min-height: 0;
}

/* Account page (2026-08-03): same fix, same reasoning — the gap between
   the "Account" H1 and Account Details' top divider read as too much dead
   space, for the same reason (#ql-error reserving space while empty). */
body.ql-account-page #ql-error {
  margin-top: 0;
  min-height: 0;
}

/* Account page (2026-08-03): the opposite request for the gap between the
   two content sections themselves — Account Details and Export Account
   Data need more separation than .ql-plain-section's default 2.5rem
   margin-top gives every other page's stack of sections. Scoped to this
   one section by id rather than raising .ql-plain-section's shared
   margin-top, which every other page using it (Settings, Data Export,
   Manual) would inherit unintentionally. */
#ql-account-data-section {
  margin-top: 4rem;
}

/* Add Client (2026-08-03): .ql-section-placeholder is margin: 0 by default
   (every other page using it has exactly one such paragraph per section),
   but this section stacks two in a row — the intro sentence and the
   capacity line — which needs a gap between them or they run together. */
#ql-add-client-intro {
  margin-bottom: 0.75rem;
}

/* Same rhythm below the capacity line — .ql-button has no margin of its
   own (by design, since it's reused all over the site with very different
   spacing needs), so without this the toggle button sat almost flush
   against "Total clients..." while the gap above it (intro -> capacity)
   was a full 0.75rem, an uneven, "off" rhythm across 3 stacked lines. */
#ql-add-client-toggle {
  margin-top: 0.75rem;
}

/* Admin page (2026-08-02): read-only Client Roster — one row per client at
   the admin's own location (plus a placeholder row per admin-only login
   with no client account), default-sorted least-recently-active first (see
   admin-service.js's compareByLeastRecentlyActive) but re-sortable by any
   column via the header buttons below. Reuses the exact same table
   language as .ql-monthly-review-table (dotted row separators, Signal Cyan
   uppercase headers, reverse-video row hover) rather than inventing a new
   table style. */
.ql-roster-table-wrap {
  overflow-x: auto;
}

/* table-layout: fixed (plus explicit per-column widths below) — auto
   layout let Lifetime Points balloon to absorb the table's own leftover
   width even though its actual content (a short number) never needs it.
   Name gets the most room and can genuinely overflow (a long placeholder
   name), so it alone gets ellipsis truncation; every other column's
   content is short enough to never need it. */
.ql-roster-table {
  width: 100%;
  /* Real bug, reported live on mobile: table-layout:fixed's percentage
     column widths are relative to the TABLE's own width, and with only
     `width: 100%` there was nothing stopping that from shrinking below
     each column's nowrap content on a narrow viewport — columns didn't
     scroll, their text just visually overlapped the column next door.
     A min-width sized to fit every column's nowrap content comfortably
     means the table now stops shrinking there and .ql-roster-table-wrap's
     existing `overflow-x: auto` kicks in instead, exactly as it was
     already meant to for an over-wide table. */
  min-width: 40rem;
  margin: 1rem 0 0;
  border-collapse: collapse;
  table-layout: fixed;
}

/* overflow/text-overflow apply to every column, not just Name — real bug,
   reported live on mobile: "Never logged anything yet" and "Level 1 ·
   Junior"/"· Senior" (the two longer stage names) both overflowed their
   own column even after the min-width fix above, visually overlapping
   the next column exactly like the original bug. Ellipsis-truncating
   every column is the general fix; the min-width above still keeps
   ordinary content (names, "Level 6 · Mid", short dates) fully visible
   without ever needing to truncate in practice. */
.ql-roster-table th,
.ql-roster-table td {
  padding: 0.5rem 0.75rem;
  text-align: left;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Rebalanced from the original 28/12/22/13/25 split (plus the min-width
   bump above) — real bug, reported live: Level's old 22% share was too
   narrow for "Level X · Stagename" (the longest stage names, "Junior" and
   "Senior," need ~152px) at any width below desktop, so it was silently
   ellipsis-truncating to "Level 3 · …" even though nothing overlapped.
   Role and Lifetime Points only ever hold short content ("Admin"/
   "Client", a handful of digits), so they give up a few points to Level
   without ever needing to truncate themselves. */
.ql-roster-table th:first-child,
.ql-roster-table td:first-child {
  width: 26%;
}

.ql-roster-table th:nth-child(2),
.ql-roster-table td:nth-child(2) {
  width: 11%;
}

.ql-roster-table th:nth-child(3),
.ql-roster-table td:nth-child(3) {
  width: 27%;
}

.ql-roster-table th:nth-child(4),
.ql-roster-table td:nth-child(4) {
  width: 12%;
}

.ql-roster-table th:nth-child(5),
.ql-roster-table td:nth-child(5) {
  width: 24%;
}

.ql-roster-table th {
  border-bottom: 1px solid var(--ql-accent);
  padding: 0;
}

.ql-roster-table td:not(:first-child) {
  font-variant-numeric: tabular-nums;
}

.ql-roster-table td.ql-roster-cell--numeric {
  text-align: right;
}

.ql-roster-table tbody tr:last-child td {
  border-bottom: none;
}

.ql-roster-table tbody tr:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

/* A real <button>, not just a styled <th>, so column sorting is keyboard-
   and screen-reader-operable for free — full-width/height of its cell so
   the whole header area is clickable, not just the text. */
.ql-roster-sort {
  display: block;
  width: 100%;
  padding: 0.5rem 0.75rem;
  background: transparent;
  border: none;
  cursor: pointer;
  font-family: 'VT323', monospace;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ql-roster-sort:hover,
.ql-roster-sort:focus-visible {
  color: var(--ql-accent);
  text-shadow: 0 0 4px rgba(57, 255, 138, 0.5);
}

/* A row for an admin-only login with no client account — the name cell is
   placeholder text (no real name exists anywhere for this kind of login),
   set apart with muted italics rather than looking like a real client. */
.ql-roster-row--placeholder td {
  color: var(--ql-muted);
  font-style: italic;
}

/* Admin page — Add Client (2026-08-03): the capacity line ("Total clients:
   x/10...") stays visible always; everything else (the intro sentence and
   the form) hides behind this bracketed toggle until clicked, so the
   section stays quiet when nobody needs it. grid-template-rows 0fr->1fr is
   a height-auto reveal with no JS measuring; the inner overflow:hidden is
   what actually clips the collapsed content. */
.ql-reveal {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}

.ql-reveal.ql-is-open {
  grid-template-rows: 1fr;
}

.ql-reveal > div {
  overflow: hidden;
}

.ql-reveal-inner {
  padding-top: 1rem;
}

@media (prefers-reduced-motion: reduce) {
  .ql-reveal {
    transition: none;
  }
}

/* Add Client fields as a live terminal prompt sequence (`> LABEL: value`)
   instead of a boxed input with a label above it — picked over two other
   mockup directions (a roster-embedded row, a modal dialog) shown to the
   user first. Borderless at rest, on purpose — the line itself reads as
   the "prompt", not a form control — but still gets the site's standard
   focus-visible outline below, since a visible focus indicator shouldn't
   be sacrificed for the aesthetic. Label width is fixed wide enough to fit
   the longest label in either language ("Naam van de Cliënt") without
   wrapping, so EN/NL toggle doesn't reflow the column. */
.ql-prompt-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.35rem 0;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.25);
}

/* Settings' Add a New Fact form has a multi-row textarea, unlike Add
   Client's single-line inputs — baseline alignment would sink the caret
   and label to the textarea's own baseline, so this variant aligns them
   to its top instead. flex-wrap plus the textarea's own flex: 0 0 100%
   below puts it on its own line under "> Guidance Message:" rather than
   squeezed beside the label (real bug, reported live with a screenshot —
   it read fine for the single-line Fact Name row above it, but a textarea
   needs its own full-width line to be usable). Doubled selector
   (`.ql-prompt-row.ql-prompt-row--textarea textarea`) is deliberate: the
   shared `.ql-prompt-row textarea { flex: 1; ... }` rule further down
   this file sets flex-basis: 0% (part of the `flex: 1` shorthand) at
   equal specificity to a single-class version of this selector, so
   without the extra specificity here, source order let that rule quietly
   win — flex-shrink then let the "full width" textarea shrink right back
   down to whatever space was left on the label's own line instead of
   actually wrapping (caught via exact DOM measurement, not just a
   screenshot — it can look right at a glance while still being wrong). */
.ql-prompt-row--textarea {
  align-items: flex-start;
  flex-wrap: wrap;
}

.ql-prompt-row.ql-prompt-row--textarea textarea {
  flex: 0 0 100%;
  margin-top: 0.5rem;
}

.ql-prompt-caret {
  color: var(--ql-accent-cyan);
  flex-shrink: 0;
}

.ql-prompt-row label {
  flex-shrink: 0;
  width: 11rem;
  color: var(--ql-muted);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-size: 1.05rem;
}

.ql-prompt-row label::after {
  content: ':';
}

.ql-prompt-row input,
.ql-prompt-row textarea {
  width: 100%;
  font-family: 'VT323', monospace;
  font-size: 1.1rem;
  background: transparent;
  border: none;
  color: var(--ql-text);
  padding: 0.1rem 0;
}

.ql-prompt-row textarea {
  resize: vertical;
}

.ql-prompt-row input:focus-visible,
.ql-prompt-row textarea:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

/* The 3rd item in a .ql-prompt-row's flex layout is either a plain input
   (name/email), a textarea (Settings' Add a New Fact), or, for a password
   field, the .ql-password-field wrapper below — either way it needs to be
   the one flex item that grows. */
.ql-prompt-row input,
.ql-prompt-row textarea,
.ql-prompt-row .ql-password-field {
  flex: 1;
  min-width: 0;
}

/* Shared show/hide password toggle — the Admin Panel's Add Client
   password field and the Account page's Change Password fields. An
   eye/eye-off SVG icon (public/js/password-toggle.js), currentColor so it
   inherits this button's own muted-at-rest/accent-on-hover color exactly
   like every other quiet, secondary affordance in this system. */
.ql-password-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}

.ql-password-field input {
  flex: 1;
  min-width: 0;
}

/* Login and Account's Change Password fields sit inside a plain .ql-field,
   which normally borders the input itself — that left the eye icon
   floating outside the box, beside a field narrower than its sibling
   Email input. Add Client's password field doesn't need this override: its
   .ql-prompt-row ancestor is deliberately borderless already. Moving the
   border/background to the wrapper and stripping the input's own border
   makes the icon read as part of the same box, and .ql-password-field's
   existing `width: 100%` keeps it exactly as wide as Email. */
.ql-field .ql-password-field {
  border: 1px solid var(--ql-accent);
  background: var(--ql-bg);
}

.ql-field .ql-password-field input {
  border: none;
  background: transparent;
}

.ql-field .ql-password-field input:focus-visible {
  outline: none;
}

.ql-field .ql-password-field:focus-within {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

.ql-password-toggle {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--ql-muted);
  cursor: pointer;
  padding: 0.3rem;
}

.ql-password-toggle:hover,
.ql-password-toggle:focus-visible {
  color: var(--ql-accent);
}

.ql-password-toggle:focus-visible {
  outline: 3px solid var(--ql-accent-cyan);
  outline-offset: 2px;
}

/* Same stacking breakpoint every other multi-column layout on the site
   uses — the fixed label width doesn't have room to sit beside the input
   on a phone-width screen. */
@media (max-width: 768px) {
  .ql-prompt-row {
    flex-wrap: wrap;
  }

  .ql-prompt-row label {
    width: auto;
  }

  .ql-prompt-row input {
    flex-basis: 100%;
  }
}

/* Live password-requirement checklist under the Add Client form's password
   field — one line per rule (Supabase project's actual policy: 8+ chars,
   upper, lower, digit), each muted (neutral, not a warning color) until
   met, flipping to Terminal Green with a checkmark the instant that one
   rule is satisfied. Deliberately never red/warning-styled even while
   unmet — this product's stats are gain-only by design (see CLAUDE.md/PRD
   3.1), and that same restraint extends to form validation: nothing here
   reads as an error until the user actually tries to submit. */
/* Scoped to .ql-plain-section specifically (not just .ql-password-
   requirements alone) — this list lives inside a .ql-plain-section, and
   .ql-plain-section ul/li further down this file (the Manual page's prose
   lists: 1.5rem indent, 0.5rem line spacing) outrank a single-class
   selector on specificity regardless of source order. */
.ql-plain-section .ql-password-requirements {
  list-style: none;
  margin: 0.35rem 0 1rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.ql-plain-section .ql-password-requirements li {
  margin-bottom: 0;
}

/* Admin Panel only: indented to roughly the caret+gap width of the
   .ql-prompt-row above it, so this reads as system output following the
   PASSWORD: prompt rather than a fifth, unrelated prompt line of its own.
   The Account page's Change Password form uses a plain .ql-field (no
   caret), so it deliberately stays flush-left via the general rule above. */
#ql-add-client-reveal .ql-password-requirements {
  margin-left: 1.75rem;
}

.ql-field-hint {
  margin: 0;
  font-size: 0.9rem;
  color: var(--ql-muted);
}

.ql-field-hint--met {
  color: var(--ql-accent);
}

/* Settings page (2026-07-12): Facts & Guidance. Same "unframed staff tool"
   treatment as Data Export (.ql-plain-page). Scoring Rules (an editable
   points-per-item admin panel) shipped alongside this originally but was
   pulled from the UI the same day after a conversation with a social
   worker — see MEMORY.md. The backend (scoring_rules table, settings
   routes/service, and scoring-service.js's live-rules wiring) is left in
   place, unused, so re-adding the UI later needs no backend work; only
   the markup/JS/strings/CSS specific to that section were removed. */
.ql-settings-intro {
  color: var(--ql-muted);
  font-size: 1.05rem;
  margin: 0 0 1rem;
}

.ql-settings-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.ql-settings-row__status {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  min-width: 4ch;
}

.ql-settings-row__status--success {
  color: var(--ql-accent);
}

.ql-settings-row__status--error {
  color: var(--ql-accent-cyan);
}

/* Facts & Guidance: one row per fact — name is fixed (see
   settings-service.js), only the guidance wording is editable. */
.ql-settings-list--facts {
  gap: 1.25rem;
  margin-bottom: 1.5rem;
}

.ql-settings-fact-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.75rem;
}

.ql-settings-fact-row__name {
  flex: 0 0 10rem;
  font-size: 1.15rem;
  color: var(--ql-text);
  padding-top: 0.4rem;
}

.ql-settings-fact-row__message {
  flex: 1 1 20rem;
  font-family: 'VT323', monospace;
  font-size: 1.05rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--ql-accent);
  border-radius: 0;
  background: var(--ql-bg);
  color: var(--ql-text);
  resize: vertical;
}

.ql-settings-add-fact {
  padding-top: 1.5rem;
  border-top: 1px solid rgba(57, 255, 138, 0.35);
}

/* rows="2" (set in settings.js) is sized for desktop, where this column is
   wide enough that most guidance messages still fit on 1-2 visual lines
   despite the 2-row box. On a phone-width column the same text wraps to
   3 lines and the 3rd was silently clipped inside the fixed-height
   textarea — real bug, caught during a mobile pass (the message is still
   there and scrollable, but nothing on screen hints at that). A taller
   floor here fits a typical short guidance message without clipping;
   resize: vertical (already set above) still lets staff drag it taller
   for a longer one. */
@media (max-width: 768px) {
  .ql-settings-fact-row__message {
    min-height: 4.75rem;
  }
}

.ql-modal-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.75);
  z-index: 1002;
}

/* Combined selector so this always wins over .ql-modal-overlay's own
   `display: flex`, regardless of which rule appears later in the file —
   two equal-specificity rules on the same element would otherwise let
   source order silently decide (bug: modal stayed visible even with this
   class applied, since .ql-modal-overlay was declared after .ql-hidden). */
.ql-modal-overlay.ql-hidden {
  display: none;
}

/* Same double-border terminal-window language as the main frame — the
   earlier clip-path stepped-corner card (plus a separate offset-shadow
   duplicate element to fake depth) was exactly the kind of "flat CSS
   shape faking pixel art" the redesign was asked to fix. Still true for
   the Boss Battle victory modal (public/boss.html), which uses these base
   rules unmodified. The Obstacle guidance modal below is a deliberate,
   scoped exception to this same history — see .ql-guidance-modal. */
.ql-modal-wrap {
  width: 85%;
  max-width: 320px;
}

.ql-modal {
  background: var(--ql-panel);
  border: 4px double var(--ql-accent);
  box-shadow: 0 0 40px rgba(57, 255, 138, 0.15);
  padding: 1.75rem;
  text-align: left;
}

.ql-modal-message {
  color: var(--ql-text);
  font-family: 'VT323', monospace;
  font-size: 1.25rem;
  line-height: 1.4;
  margin: 0 0 1.25rem;
}

.ql-modal-divider {
  border: none;
  border-top: 1px dotted rgba(57, 255, 138, 0.4);
  margin: 0 0 1.25rem;
}

/* Manual food-entry modal ("Can't find it? Log it manually") needs more
   room than the base 320px cap — 6 form fields, not one or two lines of
   message text. Scoped override, same pattern as the HUD modals below:
   extra width via an added class, never editing the shared base rule. */
.ql-manual-food-modal-wrap {
  max-width: 420px;
}

/* Shared "HUD" modal treatment — scoped sci-fi/HUD exception (2026-07-17,
   direct user request from a reference image, iterated over 3 rounds of
   mockups on the Obstacle guidance modal first, then applied to the Boss
   Battle victory modal too — same request, second component). A
   deliberate exception to two standing rules: the One-Voice Rule (VT323
   everywhere) and the retired clip-path "stepped-corner card" look
   documented above. Applied via extra classes (.ql-hud-modal-wrap,
   .ql-hud-modal, .ql-hud-corner, .ql-hud-close) added alongside each
   modal's existing .ql-modal-wrap/.ql-modal/.ql-button classes, never by
   editing the shared base rules — so any *other* future modal reusing
   .ql-modal-overlay/.ql-modal-wrap/.ql-modal still gets the original
   double-border square-corner Terminal Window look by default, and has to
   opt in to this treatment explicitly.
   Uses Chakra Petch (added to the @import at the top of this file) for a
   genuinely technical/HUD character rather than faking it with
   letter-spacing on Inter — chosen over the flashier Orbitron
   specifically because these modals show real sentences that need to
   stay legible, not just a display headline (PRODUCT.md's "legible
   before decorative" principle). Set once on .ql-hud-modal and inherited
   by every child (badge stamp, victory log, guidance message) rather than
   repeated per element.
   Chamfered corners are built from two nested clip-path layers rather
   than a border, same trick as a CSS "gradient border": the wrap is a
   green→near-black gradient shape with a thin 1.5px padding, the inner
   .ql-hud-modal is a slightly smaller clip of the same polygon sitting on
   top, leaving a 1.5px gradient sliver as the "border". The glow is split
   two ways: a soft ambient bloom outside the wrap, and a stronger inward
   glow on the panel that falls off hard toward the center, so the panel
   reads almost void-black at its core with a bright green rim — closer to
   the reference image than a flat glow. Frame is sized up (from a 320px
   cap to 420px, more generous padding) without scaling the type — a
   direct "bigger frame, same text size" request. */
.ql-hud-modal-wrap {
  clip-path: polygon(
    24px 0,
    100% 0,
    100% calc(100% - 24px),
    calc(100% - 24px) 100%,
    0 100%,
    0 24px
  );
  background: linear-gradient(180deg, var(--ql-accent) 0%, rgba(0, 20, 12, 0.15) 100%);
  padding: 1.5px;
  box-shadow: 0 0 24px rgba(57, 255, 138, 0.28);
  max-width: 420px;
}

.ql-hud-modal {
  clip-path: polygon(
    22.5px 0,
    100% 0,
    100% calc(100% - 22.5px),
    calc(100% - 22.5px) 100%,
    0 100%,
    0 22.5px
  );
  background-color: #030d08;
  background-image:
    linear-gradient(rgba(57, 255, 138, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(57, 255, 138, 0.1) 1px, transparent 1px);
  background-size: 12px 12px;
  border: none;
  box-shadow:
    inset 0 0 55px rgba(57, 255, 138, 0.4),
    inset 0 0 90px rgba(0, 0, 0, 0.85);
  padding: 2.4rem 2.2rem;
  position: relative;
  font-family: 'Chakra Petch', system-ui, sans-serif;
}

.ql-hud-corner {
  position: absolute;
  font-size: 0.6rem;
  letter-spacing: 0.02em;
  color: var(--ql-muted);
  opacity: 0.7;
  pointer-events: none;
}

.ql-hud-corner--tl {
  top: 14px;
  left: 30px;
}

.ql-hud-corner--br {
  bottom: 14px;
  right: 30px;
}

/* Deliberately small and quiet, not a real heading — direct follow-up
   request after the first pass read as too loud ("less obvious"). */
.ql-guidance-eyebrow {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ql-muted);
  margin: 0 0 0.6rem;
}

/* font-family repeated here deliberately, not left to inherit from
   .ql-hud-modal: .ql-modal-message's own base rule (above) sets
   font-family directly on this same element, and a direct declaration on
   an element is never overridden by an ancestor's inherited value — only
   a rule that also declares the property on this exact element can win.
   Real bug caught live on blip-up.nl: this rule originally omitted
   font-family assuming inheritance would supply it, and the message
   rendered in VT323 instead of Chakra Petch as a result. */
.ql-guidance-modal .ql-modal-message {
  font-family: 'Chakra Petch', system-ui, sans-serif;
  font-size: 0.95rem;
  line-height: 1.55;
  margin: 0 0 1.4rem;
}

/* Overrides .ql-button's bracketed-command styling (font, brackets) for
   any modal using the shared HUD treatment, keeping its reverse-video
   hover/focus/active/disabled states — restyled visuals, same established
   interaction feedback. font-family repeated here for the same reason as
   .ql-guidance-modal .ql-modal-message above: .ql-button's own base rule
   sets font-family directly on this same button, so it must be
   overridden explicitly rather than left to inherit — same live bug,
   same fix, on both the guidance and Boss victory close buttons. */
.ql-hud-close {
  font-family: 'Chakra Petch', system-ui, sans-serif;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  /* A flex block defaults to filling its container's width, so plain
     margin: auto has nothing to center against — width: fit-content
     shrinks it back to its content first. */
  width: fit-content;
  margin: 0 auto;
}

.ql-hud-close::before {
  content: '▸';
}

.ql-hud-close::after {
  content: none;
}

/* Page-level HUD frame — Profile/Boss Battle's own version of the
   .ql-hud-modal-wrap/.ql-hud-modal treatment above, scaled to full page
   width instead of a ~420px popup. Applied via .ql-hud-page-wrap (new
   wrapper) + .ql-hud-page (added alongside .ql-panel--wide's existing
   classes on profile.html/boss.html's <main>) — .ql-panel--wide itself is
   untouched since Today's Quest also uses that class and doesn't want any
   of this.
   Two real nested elements (not a border/background-origin fake-border on
   one element) for the same reason the modal itself uses two: `border` is
   fundamentally rectangular, so wherever a clip-path chamfer slices
   diagonally through a corner there's no border geometry for a
   background-origin trick to reveal — confirmed with an isolated test
   page, the diagonal rendered as a hard cut with no visible border line
   while the straight edges looked fine. Two independently-clipped shapes
   sidesteps this: the "border" is just the color difference between the
   two, so it's exactly as wide through a diagonal as anywhere else.
   .ql-hud-page-wrap carries the real width/max-width .ql-panel--wide
   normally has — .ql-page (its own parent) is a flex item that
   shrink-wraps to content and doesn't reliably match .ql-panel--wide's
   90%-width box on its own, so the width has to live on whichever element
   is .ql-page's actual child. */
/* -webkit-clip-path + backface-visibility:hidden fixes a real bug
   reported live on an actual iOS Safari phone (not reproducible via
   desktop dev-tools emulation): the top-left chamfer rendered correctly
   on first paint, then "flickered" back to a plain right angle — a known
   WebKit issue where clip-path fails to properly re-composite after a
   post-paint reflow (here, the VT323/Chakra Petch webfonts finishing
   their async load and shifting text heights slightly).
   Deliberately NOT `transform: translateZ(0)` (the other half of this
   classic fix pair) — this element is an ancestor of the fixed-position
   `.ql-avatar-picker__panel`, and any `transform` other than `none` makes
   its element the *containing block* for fixed/absolute descendants
   (CSS spec), which broke the panel's viewport-centered positioning when
   tried here (caught before shipping). `backface-visibility` alone isn't
   in that containing-block list, so it still nudges this onto its own
   compositing layer without that side effect. */
.ql-hud-page-wrap {
  width: 92%;
  max-width: 1320px;
  margin: 0 auto;
  -webkit-clip-path: polygon(
    28px 0,
    100% 0,
    100% calc(100% - 28px),
    calc(100% - 28px) 100%,
    0 100%,
    0 28px
  );
  clip-path: polygon(
    28px 0,
    100% 0,
    100% calc(100% - 28px),
    calc(100% - 28px) 100%,
    0 100%,
    0 28px
  );
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  background: linear-gradient(180deg, var(--ql-accent) 0%, rgba(0, 20, 12, 0.15) 100%);
  padding: 2px;
  box-shadow: 0 0 40px rgba(57, 255, 138, 0.25);
}

/* body.ql-unboxed .ql-hud-page (not plain .ql-hud-page) is deliberate:
   body.ql-unboxed .ql-panel { background:transparent; box-shadow:none; }
   above has specificity (0,2,1) — a plain single-class .ql-hud-page rule
   at (0,1,0) would lose to it regardless of source order and this new
   background/glow would never render. Matching that same (0,2,1)
   specificity here means this later rule correctly wins on source order
   instead, the same tie-breaking pattern already used elsewhere in this
   file (e.g. body.ql-unboxed .ql-log-grid .ql-section). */
body.ql-unboxed .ql-hud-page {
  position: relative;
  width: 100%;
  max-width: none;
  -webkit-clip-path: polygon(
    26px 0,
    100% 0,
    100% calc(100% - 26px),
    calc(100% - 26px) 100%,
    0 100%,
    0 26px
  );
  clip-path: polygon(
    26px 0,
    100% 0,
    100% calc(100% - 26px),
    calc(100% - 26px) 100%,
    0 100%,
    0 26px
  );
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  background-color: #030d08;
  background-image:
    linear-gradient(rgba(57, 255, 138, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(57, 255, 138, 0.1) 1px, transparent 1px);
  background-size: 14px 14px;
  box-shadow:
    inset 0 0 70px rgba(57, 255, 138, 0.35),
    inset 0 0 110px rgba(0, 0, 0, 0.85);
}

/* The titlebar's default padding-left (1.5rem/24px) sits almost exactly
   where the 26px chamfer notch ends, crowding "QuestLog" right into the
   diagonal cut — extra left padding, scoped to this frame only, clears
   it. */
.ql-hud-page .ql-titlebar {
  padding-left: 2.5rem;
}

/* Sits outside the frame entirely — a real sibling above
   .ql-hud-page-wrap in normal document flow (both flex items of the same
   .ql-page column), not absolutely positioned over the panel. Direct
   follow-up request after a first pass placed it inside the frame,
   overlapping the titlebar text in that same crowded top-left corner the
   padding-left fix above addresses. Same width as the wrap so its left
   edge lines up with the frame's own left edge, reading as a caption
   above it. */
.ql-hud-page-label {
  display: block;
  width: 92%;
  max-width: 1320px;
  margin: 0 auto 0.4rem;
  font-family: 'Chakra Petch', system-ui, sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--ql-muted);
  opacity: 0.8;
}

/* Bottom-right decorative code stays inside the frame, reusing the exact
   .ql-hud-corner treatment the modal already established — the
   compound selector wins over .ql-hud-corner--br's own modal-tuned
   position (equal-specificity source order would be fragile; this is
   explicit). */
.ql-hud-page .ql-hud-corner--br {
  bottom: 14px;
  right: 30px;
}

.ql-logout {
  margin-top: 1.75rem;
}

/* Cross-page nav row (index/profile/boss) — same bracket-button language
   as every other action in the app. Now a flex child inside .ql-titlebar
   (single-row header, 2026-07-22) rather than its own row below it, so it
   no longer carries its own outer padding/margin — .ql-titlebar's padding
   covers it. */
.ql-nav {
  display: flex;
  gap: 1rem;
}

/* Mobile-only page-switcher trigger (page-nav.js/topbar.js's
   initPageNav()) — hidden on desktop/tablet, where #ql-nav renders as the
   plain 3-button row it always has. See the max-width:768px media query
   for its visible styling. */
.ql-nav-toggle {
  display: none;
}

/* Generic horizontal padding wrapper for page content that isn't already
   inside .ql-layout (which carries its own padding) — used by the
   Profile/Boss pages, which don't have a 2-column quest/log split. */
.ql-page-content {
  padding: 0 1.75rem 0.5rem;
}

.ql-section--center {
  text-align: center;
}

/* Food/Boss Badges (PRD 13.1) — small bracketed tiles, one per earned
   badge. Deliberately plain (single 1px border, no glow on the tile itself)
   so a wall of badges doesn't visually compete with the outer
   double-bordered window. */
.ql-badge-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
}

/* Boss Badges — text-only (a boss name + level, not a food). */
.ql-badge {
  border: 1px solid var(--ql-accent);
  padding: 0.4rem 0.6rem;
  font-size: 0.95rem;
}

/* Food Badges — icon stacked above the name. Two tiers (see
   public/js/food-icons.js and DESIGN.md "Food Badge Icon Colors"):
   .ql-food-icon--category is the hand-drawn single-color pixel icon,
   colored via a per-category modifier class + CSS currentColor. A bare
   .ql-food-icon (no --category class) is a specific per-food icon sourced
   with its own original colors — those are left alone entirely, no fill
   override, no glow, so their own art renders as-is. Everything else on
   the tile (border, name text) stays the standard Terminal Green/Frost
   Text regardless of tier. */
.ql-food-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  width: 76px;
  border: 1px solid var(--ql-accent);
  padding: 0.5rem 0.35rem 0.4rem;
  text-align: center;
}

.ql-food-badge__name {
  font-size: 0.8rem;
  line-height: 1.15;
  word-break: break-word;
}

.ql-food-icon {
  width: 32px;
  height: 32px;
}

.ql-food-icon svg {
  width: 100%;
  height: 100%;
}

.ql-food-icon--category {
  color: var(--ql-accent);
  filter: drop-shadow(0 0 3px currentColor);
}

.ql-food-icon--category svg {
  fill: currentColor;
}

.ql-food-icon--category.ql-food-icon--vegetable {
  color: var(--ql-accent);
}

.ql-food-icon--category.ql-food-icon--dairy {
  color: var(--ql-accent-cyan);
}

.ql-food-icon--category.ql-food-icon--fruit {
  color: var(--ql-food-fruit);
}

.ql-food-icon--category.ql-food-icon--protein {
  color: var(--ql-food-protein);
}

.ql-food-icon--category.ql-food-icon--grain {
  color: var(--ql-food-grain);
}

.ql-food-icon--category.ql-food-icon--generic {
  color: var(--ql-accent);
}

/* Victory pop-up (defeated-state, 2026-07-15, moved from an inline banner
   to a real modal same day) — a defeated boss previously reused the exact
   same layout/copy as an active fight with only a small text line marking
   the win (`/impeccable critique` P2). Reuses the app's one modal shape
   (`.ql-modal-overlay`/`.ql-modal-wrap`/`.ql-modal`, same as the obstacle
   guidance pop-up, including its dialog semantics/focus-trap/Escape
   pattern — see boss.js) rather than a second bespoke popup treatment.
   Ties the win to the badge the client actually just earned (visible
   later on Profile) — the stamp shows the same boss name/epithet/level as
   the title on the page behind it, tilted like a freshly-applied seal. */
.ql-boss-victory-modal {
  text-align: center;
}

.ql-boss-badge-stamp {
  display: inline-flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 200px;
  border: 3px double var(--ql-accent);
  background: var(--ql-bg);
  padding: 1rem 1.25rem 0.85rem;
  transform: rotate(-1.4deg);
  box-shadow: 0 0 22px rgba(57, 255, 138, 0.25);
}

.ql-boss-badge-stamp__eyebrow {
  font-size: 0.95rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.6);
}

.ql-boss-badge-stamp__name {
  font-size: 1.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.6);
  line-height: 1.15;
}

.ql-boss-badge-stamp__level {
  font-size: 1rem;
  letter-spacing: 0.08em;
  color: var(--ql-muted);
}

/* Level-Up / Stage-Up celebration modal (index.html/app.js, PRD 16.6) —
   deliberately a different frame from Boss-Defeat's badge-stamp (2026-07-27,
   direct feedback: reusing the same stamp for all three read as
   repetitive, and a scaled-up stamp didn't make Stage-Up feel meaningfully
   bigger). No solid border or background panel at all — 4 floating corner
   brackets, like a HUD reticle locking onto the moment, reading as "the
   system tracking your growth" rather than Boss-Defeat's "official stamped
   record." Overrides .ql-modal's own double-border base rule entirely
   (this modal doesn't want it), same "combine base class + later override"
   pattern .ql-hud-modal already relies on.
   The eyebrow/name/level text still reuses .ql-boss-badge-stamp__* purely
   for its typography — just not wrapped in the .ql-boss-badge-stamp box,
   since there's no box here. Entrance animation (bracket scale-in, text
   fade-up, and — only on a genuine stage crossing — the cyan sweep +
   dual-glow below) is GSAP-driven from app.js, not CSS; this file only
   holds the frame's resting geometry and the reduced-motion end-state. */
.ql-viewfinder-wrap {
  max-width: 360px;
}

.ql-viewfinder-modal {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 2.4rem 3rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  text-align: center;
  font-family: 'Chakra Petch', system-ui, sans-serif;
  /* For the Stage-Up avatar flip (app.js) — a real 3D rotateY needs a
     perspective on its containing block to read as a card turning over
     rather than a flat horizontal squash. */
  perspective: 400px;
}

/* Stage-Up only (PRD 16.6) — hidden by default (.ql-hidden) so a plain
   Level-Up never shows it; app.js reveals + flips it only when a stage
   boundary was actually crossed, from the client's pre-crossing stage art
   to their new one. */
.ql-viewfinder-avatar {
  width: 72px;
  height: 72px;
  image-rendering: pixelated;
  margin-bottom: 0.3rem;
}

.ql-viewfinder-bracket {
  position: absolute;
  width: 26px;
  height: 26px;
  border: 2px solid var(--ql-accent);
  filter: drop-shadow(0 0 4px rgba(57, 255, 138, 0.6));
}

.ql-viewfinder-bracket--tl {
  top: 0;
  left: 0;
  border-right: none;
  border-bottom: none;
}

.ql-viewfinder-bracket--tr {
  top: 0;
  right: 0;
  border-left: none;
  border-bottom: none;
}

.ql-viewfinder-bracket--bl {
  bottom: 0;
  left: 0;
  border-right: none;
  border-top: none;
}

.ql-viewfinder-bracket--br {
  bottom: 0;
  right: 0;
  border-left: none;
  border-top: none;
}

/* Cyan scan-sweep — the "second glow color" that marks a Stage-Up apart
   from a plain Level-Up (Two-Accent Rule: cyan is the deliberate second
   accent here, not decoration). Zero width at rest; GSAP animates it
   sweeping left-to-right only when a stage boundary was actually crossed. */
.ql-viewfinder-sweep {
  position: absolute;
  top: 0;
  left: -15%;
  width: 3px;
  height: 100%;
  background: linear-gradient(180deg, transparent, var(--ql-accent-cyan), transparent);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .ql-viewfinder-bracket {
    filter: drop-shadow(0 0 4px rgba(57, 255, 138, 0.6));
  }
  .ql-viewfinder-sweep {
    display: none;
  }
}

/* Terminal print-line treatment, not prose — same "> " prompt-glyph
   convention as .ql-section-heading. display:inline-block centers the
   line as a unit inside the centered modal while keeping the text itself
   left-aligned, like a real readout rather than a stretched centered
   sentence. */
.ql-boss-victory-log {
  display: inline-block;
  text-align: left;
  font-size: 1.1rem;
  color: var(--ql-accent);
  margin: 1.1rem auto 0;
}

.ql-boss-victory-log::before {
  content: '> ';
}

.ql-boss-victory-log::after {
  content: '';
  display: inline-block;
  width: 0.55em;
  height: 1em;
  margin-left: 3px;
  background: var(--ql-accent);
  vertical-align: text-bottom;
  animation: ql-cursor-blink 1s steps(1) infinite;
}

@keyframes ql-cursor-blink {
  50% {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ql-boss-victory-log::after {
    animation: none;
  }
}

/* Dims + desaturates the same boss art rather than requiring a second,
   defeated-specific illustration asset — a stilled, spent look instead of
   the mid-fight pose, paired with the banner above leading with the win. */
.ql-boss-content--defeated #ql-boss-figure {
  filter: grayscale(1) brightness(0.55);
}

/* Boss title (PRD 13.2/13.3) — highlights the boss's own proper name
   ("Ignis") over its epithet ("the Overheater") and level, rather than
   rendering all three as one flat, equally-weighted string. No `>` prefix
   here (unlike .ql-section-heading elsewhere) — this reads as a title
   card, not a command. */
.ql-boss-title {
  margin: 0 0 1rem;
}

.ql-boss-title__row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.ql-boss-title__name {
  font-size: 2rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 10px rgba(57, 255, 138, 0.7);
  margin: 0;
}

.ql-boss-title__epithet {
  font-size: 1.4rem;
  color: var(--ql-muted);
}

.ql-boss-title__level {
  display: block;
  font-size: 1.1rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-muted);
  margin-top: 0.35rem;
}

/* The health label row, health bar, and target text stay left-aligned even
   though the boss title/avatar above them are centered (.ql-section--center
   sets align-items:center on the whole section) — align-self overrides the
   container's alignment for just these 3 flex items. Scoped under
   #ql-boss-content since .stat-bar__label-row is also used, unscoped, by
   Today's Quest's stat bars (align-self would be inert there regardless,
   since that page's layout isn't a flex container, but scoping keeps the
   intent explicit). */
#ql-boss-content .stat-bar__label-row,
#ql-boss-health,
#ql-boss-target {
  align-self: flex-start;
}

/* Boss health bar (PRD 13.2/13.3) — one box per health_bar_size slot.
   Filled boxes glow like the stat-bar fills; a depleted slot reads as an
   empty outline, echoing the paper sheet's "crossed-out slot" language. */
.ql-boss-health {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0.75rem 0 1rem;
}

.ql-boss-health__box {
  width: 1.1rem;
  height: 1.1rem;
  border: 1px solid var(--ql-muted);
}

.ql-boss-health__box--filled {
  border-color: var(--ql-accent);
  background: var(--ql-accent);
  box-shadow: 0 0 6px rgba(57, 255, 138, 0.7);
}

/* "Static Burnout" damage hit — fired once whenever a pip is newly emptied
   (a daily target was just met), on both the Boss Battle page's full bar
   and Today's Quest's Boss Watch widget below. A bright flare (like a
   filament overloading) then a hard snap to the empty/dark state, echoing
   the stat-gain "Phosphor Surge" animation's own hard-cut character. */
.ql-boss-health__box.is-losing {
  animation: ql-boss-damage-burnout 0.35s ease-out forwards;
}

@keyframes ql-boss-damage-burnout {
  0% {
    background: var(--ql-accent);
    border-color: var(--ql-accent);
    box-shadow: 0 0 6px rgba(57, 255, 138, 0.7);
  }
  35% {
    background: #eafff0;
    border-color: #eafff0;
    box-shadow: 0 0 22px rgba(57, 255, 138, 1);
  }
  100% {
    background: transparent;
    border-color: var(--ql-muted);
    box-shadow: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ql-boss-health__box.is-losing {
    animation: none;
  }
}

/* "Boss Watch" — Today's Quest's compact live preview of the current boss
   (name/level, mission, health), sitting under the Meals & Snacks/Power-
   Ups/Obstacles row inside .ql-column--log. Hidden entirely (via
   .ql-hidden) when no boss is active, same as boss.html's own emptyEl. */
.ql-boss-watch {
  margin-top: 1.5rem;
}

/* The mission changes per-boss (staff-configured, see boss-service.js's
   daily_target_label) — only the static "Current Mission:" label part is
   colored like a section heading; the dynamic text is the app's own ink
   color so it reads as content, not decoration. */
.ql-boss-watch__mission-text {
  color: var(--ql-text);
  text-shadow: none;
}

/* 3 items now (name, level, value) — justify-content: space-between
   would dump the middle one (level) with equal gaps on both sides
   instead of sitting right next to the name it belongs with. gap holds
   the minimum spacing between all 3; margin-left: auto on the value is
   what actually pushes it flush right, same visual result space-between
   gave the old 2-item row. */
.ql-boss-watch__row {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

.ql-boss-watch__name {
  font-family: 'VT323', monospace;
  font-size: 1.15rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ql-accent);
  opacity: 0.8;
}

/* Kept as its own element (not appended into .ql-boss-watch__name's own
   text) specifically so mobile's ellipsis truncation on a long boss name
   can never eat this along with it — flex-shrink: 0 here means it always
   renders in full regardless of viewport. */
.ql-boss-watch__level {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  color: var(--ql-muted);
  flex-shrink: 0;
}

.ql-boss-watch__value {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.6);
  margin-left: auto;
  flex-shrink: 0;
}

.ql-boss-watch .ql-boss-health {
  margin: 0 0 0.5rem;
}

.ql-boss-watch .ql-boss-health__box {
  width: 0.85rem;
  height: 0.85rem;
}

.ql-boss-watch__log {
  font-family: 'VT323', monospace;
  color: var(--ql-accent);
  font-size: 1rem;
  margin: 0.3rem 0 0;
  min-height: 1.3em;
}

/* Attack & Defence / How to Defeat / Cheat Code copy — real hierarchy bug:
   these four <p>s had no rule of their own at all, so they inherited
   body's raw 20px with the browser's default (tight) line-height. First
   fix shrank them to 1.05rem to land below .ql-section-heading — reverted
   same day per direct feedback: the ask was more hierarchy AND more
   readability, and shrinking the body copy works against readability.
   Body text keeps its natural size here; hierarchy instead comes from
   making the heading above it more assertive (see #ql-attack-heading/
   #ql-howto-heading below) and from the cyan "Attack:"/"Defence:"/
   "Cheat Code:" labels (.ql-boss-text-label) standing out from the
   sentence that follows. line-height 1.6 replaces the inherited "normal"
   (too tight for 2-4 line paragraphs); margin gives each paragraph
   deliberate rhythm instead of relying on the browser's default <p>
   spacing. */
#ql-boss-attack-text,
#ql-boss-defence-text,
#ql-boss-howto-text,
#ql-boss-cheat-text {
  line-height: 1.6;
  color: var(--ql-text);
  margin: 0 0 1rem;
}

#ql-boss-attack-text:last-child,
#ql-boss-defence-text:last-child,
#ql-boss-howto-text:last-child,
#ql-boss-cheat-text:last-child {
  margin-bottom: 0;
}

/* Inline "Attack:"/"Defence:"/"Cheat Code:" label at the start of each
   sentence (built in boss.js as a <strong>, not string-concatenated text,
   so it can be styled independently) — Signal Cyan per the Two-Accent
   Rule (this is an informational readout, same role as .stat-bar__value,
   not a primary action, so it stays green). No font-weight bump: VT323
   only ships a regular weight, so `bold` here would trigger the browser's
   synthetic-bold faux-weight, which looks chunky/blurry on a pixel font —
   color and glow alone carry the emphasis instead, same restraint
   .stat-bar__value already uses. */
.ql-boss-text-label {
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.5);
}

/* Attack & Defence / How to Defeat headings bumped up from the shared
   1.1rem .ql-section-heading size — scoped to these two ids, not the
   class itself, since .ql-section-heading is reused site-wide (Today's
   Quest, Profile, Settings) and this bump is specific to restoring
   hierarchy over Boss Battle's own (deliberately full-size) body text.
   1.3rem reuses the same step .stat-bar__label already uses elsewhere on
   this exact page, rather than inventing a new value; still comfortably
   below the 1.5rem page heading and the 2rem boss name above it. */
#ql-attack-heading,
#ql-howto-heading {
  font-size: 1.3rem;
}

/* Top navigation bar — a site-wide utility strip (logo, language toggle,
   How This Works / Data / Contact), separate from .ql-nav (the in-panel
   row for moving between the 3 main app pages). Only present on
   authenticated pages. Fixed to the viewport (not absolute) so it stays
   visible while scrolling — pages using it get extra top clearance via
   .ql-has-topbar below so it never overlaps the panel underneath it. */
/* Panel Surface (not Void Black) so the bar reads as its own layer above
   scrolled content, the same trick every other terminal window already
   uses to stand apart from the page background — plus a bottom border so
   the edge is unambiguous now that content passes underneath it. */
/* Real bug: How This Works' scanline-escape fix (#ql-how-it-works,
   z-index: 1000, see Elevation) sat higher than this bar's old z-index of
   10, so once that section scrolled up underneath the fixed topbar, its
   text painted through the nav instead of under it. Bumped to 1001 —
   above any content that's been deliberately lifted above the scanline
   (currently that section and the login page's Email/Password fields,
   both at 1000) — so the nav always wins against ordinary page content,
   lifted or not. Modals (.ql-modal-overlay, .ql-manual-changes-overlay)
   are bumped to 1002 in turn, so they still cover the topbar too. */
.ql-topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem 1.75rem;
  background: var(--ql-panel);
  border-bottom: 1px solid var(--ql-accent);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  z-index: 1001;
}

body.ql-has-topbar {
  padding-top: 5.5rem;
}

/* Wraps the logo + account name together so .ql-topbar's own
   `justify-content: space-between` only ever sees 2 children (this, and
   .ql-topbar__links) — without this, the name as a 3rd direct child of
   .ql-topbar got spaced apart from the logo instead of sitting next to it
   (real bug, caught live: it floated near the horizontal center of the bar). */
.ql-topbar__brand {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.ql-topbar__logo {
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.5);
}

/* The account's real name, right after the logo — "/name" reads as a
   terminal path segment (BLIP-UP/Alex R.), not a link. Same size/weight/
   dim-at-rest tint as every other topbar link (.ql-topbar__link) so it
   reads as part of the same row, just non-interactive (no hover state). */
.ql-topbar__name {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(216, 245, 232, 0.5);
}

/* Mobile-only hamburger toggle for the collapsed nav drawer — see the
   @media(max-width:768px) block near the end of this file. Hidden here so
   it never appears above that breakpoint; reuses .ql-button for its visual
   style (the [ MENU ] brackets come from .ql-button::before/::after) rather
   than inventing a second button look. */
.ql-topbar__toggle {
  display: none;
}

.ql-topbar__links {
  display: flex;
  align-items: center;
  gap: 1.75rem;
}

.ql-topbar__lang {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  letter-spacing: 0.06em;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0.35rem 0;
}

.ql-topbar__lang-sep {
  color: rgba(216, 245, 232, 0.5);
}

/* Non-current language stays dim like every other topbar link at rest, and
   brightens on hover to signal it's the click target. The current language
   stays lit permanently — same "you are here" language as the in-panel nav's
   aria-current fill and the Manual sidebar's scrollspy accent. */
.ql-topbar__lang-option {
  color: rgba(216, 245, 232, 0.5);
}

.ql-topbar__lang:hover .ql-topbar__lang-option,
.ql-topbar__lang:focus-visible .ql-topbar__lang-option {
  color: var(--ql-text);
}

.ql-topbar__lang-option.is-current {
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 6px rgba(57, 255, 138, 0.6);
}

.ql-topbar__lang:hover .ql-topbar__lang-option.is-current,
.ql-topbar__lang:focus-visible .ql-topbar__lang-option.is-current {
  color: var(--ql-accent);
}

/* Links share one hover treatment: text flips to Terminal Green with the
   same glow formula used everywhere else (Phosphor Glow Rule), plus a
   green gradient "sweep" behind the text that animates in on hover — a
   CRT-phosphor-igniting read, not a generic underline/tint. */
.ql-topbar__link {
  position: relative;
  display: inline-block;
  font-family: 'VT323', monospace;
  font-size: 1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  background: transparent;
  border: none;
  cursor: pointer;
  color: rgba(216, 245, 232, 0.5);
  padding: 0.35rem 0;
  transition:
    color 0.25s ease-out,
    text-shadow 0.25s ease-out;
}

/* Real bug, caught while verifying the Account link/name topbar change:
   .ql-topbar__link's own `display: inline-block` ties with .ql-hidden's
   `display: none` at equal specificity, and wins on source order alone
   since it's declared later in the file (line 52 vs here) — same trap as
   .ql-modal-overlay.ql-hidden and .ql-breakdown-item__detail.ql-hidden
   above. Confirmed live: #ql-topbar-admin kept `computed display: block`
   despite carrying the `ql-hidden` class for a non-admin account. This
   compound selector wins regardless of order, and covers every
   role-gated topbar link (Data Export, Account, Admin), not just Admin. */
.ql-topbar__link.ql-hidden {
  display: none;
}

.ql-topbar__link::before {
  content: '';
  position: absolute;
  inset: -0.3rem -0.6rem;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(57, 255, 138, 0.35) 50%,
    transparent 80%
  );
  background-size: 220% 100%;
  background-position: 130% 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease-out;
}

.ql-topbar__link:hover,
.ql-topbar__link:focus-visible,
.ql-topbar__dropdown:hover .ql-topbar__link--data,
.ql-topbar__dropdown:focus-within .ql-topbar__link--data {
  color: var(--ql-accent);
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 8px rgba(57, 255, 138, 0.7);
}

.ql-topbar__link:hover::before,
.ql-topbar__link:focus-visible::before,
.ql-topbar__dropdown:hover .ql-topbar__link--data::before,
.ql-topbar__dropdown:focus-within .ql-topbar__link--data::before {
  opacity: 1;
  animation: ql-topbar-sweep 0.9s ease-out;
}

@keyframes ql-topbar-sweep {
  from {
    background-position: 130% 0;
  }
  to {
    background-position: -30% 0;
  }
}

/* Admin link (2026-08-02, re-added without the border "frame" tried
   earlier the same day) — every other topbar link is dim-at-rest, bright-
   on-hover; this one stays permanently bright (same accent color, no new
   color) since it's the sole entry point into the Admin panel. Reverse-
   video on hover/focus, same "raised/selected state" language (Phosphor
   Glow Rule) as elsewhere — just no border/background box at rest. */
.ql-topbar__link--admin {
  color: var(--ql-accent);
  padding: 0.2rem 0.6rem;
  text-shadow:
    0 0 2px var(--ql-accent),
    0 0 6px rgba(57, 255, 138, 0.5);
}

.ql-topbar__link--admin:hover,
.ql-topbar__link--admin:focus-visible {
  background: var(--ql-accent);
  color: var(--ql-bg);
  text-shadow: none;
}

@media (prefers-reduced-motion: reduce) {
  .ql-topbar__link::before {
    transition: none;
    animation: none !important;
  }
}

.ql-topbar__dropdown {
  position: relative;
}

/* Small single-border terminal window for the Data submenu — one tier
   quieter than the main double-border windows, matching .ql-badge's
   "subordinate to the outer window" logic. */
.ql-topbar__menu {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 0.5rem;
  min-width: 170px;
  background: var(--ql-panel);
  border: 1px solid var(--ql-accent);
  box-shadow: 0 0 24px rgba(57, 255, 138, 0.15);
  padding: 0.35rem 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition:
    opacity 0.15s ease-out,
    transform 0.15s ease-out,
    visibility 0.15s;
}

.ql-topbar__dropdown:hover .ql-topbar__menu,
.ql-topbar__dropdown:focus-within .ql-topbar__menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .ql-topbar__menu {
    transition: none;
  }
}

.ql-topbar__menu-link {
  display: block;
  font-family: 'VT323', monospace;
  font-size: 1rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ql-text);
  padding: 0.5rem 0.9rem;
}

.ql-topbar__menu-link:hover,
.ql-topbar__menu-link:focus-visible {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

/* Simple placeholder pages (How This Works, Contact, Data Export,
   Settings) — body copy inside the standard .ql-panel frame. */
.ql-page-copy {
  text-align: left;
  padding: 0 1.75rem 0.5rem;
  color: var(--ql-text);
  line-height: 1.5;
}

.ql-page-copy p {
  margin: 0 0 1rem;
}

.ql-page-copy p:last-child {
  margin-bottom: 0;
}

.ql-coming-soon {
  display: inline-block;
  font-family: 'VT323', monospace;
  font-size: 0.9rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  border: 1px solid var(--ql-accent-cyan);
  padding: 0.25rem 0.6rem;
  margin-bottom: 1rem;
}

/* How This Works / Contact live as sections on Today's Quest, below Daily
   Records — reachable by scrolling or by jumping via the top nav bar
   (index.html#ql-how-it-works / #ql-contact). scroll-margin-top keeps the
   fixed topbar from covering the heading after a jump. #ql-how-it-works,
   #ql-contact, and .ql-history already supply their own horizontal
   padding, so the nested .ql-page-copy (built for direct use inside
   .ql-panel) doesn't need its own here. */
#ql-how-it-works,
#ql-contact {
  scroll-margin-top: 5.5rem;
}

/* Staff Manual page (2026-07-13): same unframed .ql-plain-page treatment
   as Data Export/Settings, plus a fixed left sidebar Table of Contents
   and a printable/PDF-exportable version — see the @media print block
   further down for that half. Sidebar layout and the Inter body font
   below were both picked from a mockup (see MEMORY.md 2026-07-13). */
.ql-manual-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.ql-manual-header__actions {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* Mobile-only Contents toggle for the sidebar TOC drawer — see the
   @media(max-width:768px) block near the end of this file. Hidden here so
   it never appears above that breakpoint, same pattern as
   .ql-topbar__toggle. */
.ql-manual-toc-toggle {
  display: none;
}

/* Always a flex row (heading + the mobile-only Close button below), even
   on desktop where the button stays hidden — a single flex child renders
   identically to a non-flex one, so this doesn't change the desktop
   layout at all. */
.ql-manual-sidebar__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* Mobile-only — see .ql-manual-toc-toggle's own comment above and the
   @media(max-width:768px) block for why this exists: the drawer covers
   the entire viewport when open, so there's no "outside" to click to
   close it, and the toggle button itself sits behind the now-opaque
   overlay. A real Close button rendered inside the drawer is the only
   way to close it without Escape (real bug caught live). */
.ql-manual-toc-close {
  display: none;
}

/* position: fixed (not sticky) per direct request — anchored to the
   viewport's left edge, independent of .ql-page's own centered flex
   layout, so it doesn't need to participate in that flow at all. Starts
   below the topbar (top: 5.5rem matches body.ql-has-topbar's own
   clearance) and scrolls its own contents independently if the list
   ever outgrows the viewport height. */
.ql-manual-sidebar {
  position: fixed;
  top: 5.5rem;
  left: 0;
  bottom: 0;
  width: 220px;
  overflow-y: auto;
  padding: 1.5rem 1.25rem;
  background: var(--ql-panel);
  border-right: 1px solid rgba(57, 255, 138, 0.35);
  z-index: 5;
}

.ql-manual-sidebar .ql-section-title {
  padding-top: 0;
  margin-bottom: 0.9rem;
  font-size: 1.2rem;
}

.ql-manual-sidebar .ql-section-title::before {
  content: none;
}

/* Groups the sidebar's 12 links into 5 clusters (Getting Started/Daily
   Routine/Reviewing & Data/Profile & Settings/Reference) instead of one
   flat list — real vertical rhythm (generous margin-top here vs. .ql-
   manual-sidebar li's tight margin-bottom within a group) does the work
   a divider line would otherwise need to. Reuses --ql-muted rather than a
   new token; small-caps-style sizing/tracking is the same "quiet all-caps
   label" language as .ql-how-it-works__manual-link's own eyebrow-free
   restraint elsewhere on the site. */
.ql-manual-sidebar__group-label {
  font-family: 'Inter', sans-serif;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ql-muted);
  margin: 1.1rem 0 0.5rem;
}

.ql-manual-sidebar__group-label:first-of-type {
  margin-top: 0;
}

.ql-manual-sidebar ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ql-manual-sidebar li {
  margin-bottom: 0.3rem;
}

/* Chevron prefix (`> `) dropped — real hierarchy bug, not a taste call:
   the heading text itself already leads with a number ("4. Daily Routine:
   ..."), so every link was carrying two markers doing the same job. The
   number is the only marker left. Text sits dimmed (a translucent tint of
   --ql-text, the same "quiet at rest" pattern the topbar's own default
   link color already uses) until hover/focus/active brightens it — Signal
   accent color is no longer on by default for all 12 links at once. */
.ql-manual-sidebar a {
  font-family: 'Inter', sans-serif;
  font-size: 0.9rem;
  line-height: 1.4;
  padding: 0.3rem 0.6rem;
  margin: 0 -0.6rem;
  border-left: 2px solid transparent;
  color: rgba(216, 245, 232, 0.7);
  text-decoration: none;
  display: block;
}

.ql-manual-sidebar a:hover,
.ql-manual-sidebar a:focus-visible {
  color: var(--ql-text);
  border-left-color: rgba(57, 255, 138, 0.4);
}

/* Current section, tracked live by an IntersectionObserver in manual.js
   as the reader scrolls — the sidebar becomes a "you are here" map
   instead of a static table of contents. Left accent bar + tinted row +
   full Terminal Green, same reverse-video-adjacent, persistent-state
   language as the [ Today's Quest ]/[ Profile ]/[ Boss Battle ] nav's own
   aria-current fix. */
.ql-manual-sidebar a.is-active {
  color: var(--ql-accent);
  background: rgba(57, 255, 138, 0.1);
  border-left-color: var(--ql-accent);
  text-shadow: 0 0 3px rgba(57, 255, 138, 0.4);
}

/* The sidebar is fixed/out-of-flow, so main content just needs its own
   left clearance instead of .ql-plain-page's usual auto-centering — a
   full override (not just a margin tweak) so it doesn't inherit that
   page's width:80%-against-an-indeterminate-parent chain at all. */
body.ql-manual-page .ql-plain-page {
  width: auto;
  max-width: 900px;
  margin: 1.5rem 2rem 2rem 260px;
}

/* Anchor-jump targets for the sidebar's links above — same
   topbar-clearance fix as #ql-how-it-works/#ql-contact. */
.ql-manual-page section[id] {
  scroll-margin-top: 5.5rem;
}

.ql-plain-section ol,
.ql-plain-section ul {
  margin: 0 0 1rem;
  padding-left: 1.5rem;
}

.ql-plain-section li {
  margin-bottom: 0.5rem;
  line-height: 1.5;
}

/* Body copy on the Manual page reads in Inter, not VT323 — chosen from a
   mockup after the user found long-form VT323 hard to read (see the
   @import comment at the top of this file for the full rationale).
   Headings/sidebar stay VT323; only paragraphs and list items change. */
body.ql-manual-page .ql-plain-section p,
body.ql-manual-page .ql-plain-section li {
  font-family: 'Inter', sans-serif;
  font-size: 1rem;
  line-height: 1.65;
}

/* Real screenshots of the app, one per major step (2026-07-13) — mocked
   realistic data captured via Playwright, not the client's real records.
   --size-* caps width for portrait/tall captures (login form, the stat
   bars, a single checkbox row) that would otherwise stretch awkwardly
   across the full ~900px content column; the default (no size modifier)
   is full width, for wide captures (a form section, a chart, a table).
   --float-* (added when the layout editor's "2 columns" option is picked
   — see further down) floats the figure so surrounding paragraphs/lists
   wrap beside it instead of stacking above/below. Panel Surface
   background + a thin border reads as "framed illustration," not a
   random pasted image. */
.ql-manual-figure {
  margin: 1.25rem 0 1.5rem;
  padding: 0.75rem;
  border: 1px solid rgba(57, 255, 138, 0.35);
  background: var(--ql-panel);
}

/* Float rules carry position/spacing only, not width — a fallback
   max-width covers the (currently unreachable via the editor's own
   presets, which always pair a size) case of a floated figure with no
   size class. The three --size-* rules are declared after these on
   purpose: equal specificity means source order decides a tie, so a
   figure combining e.g. --size-small with --float-left still ends up
   280px wide, not the float's 45% fallback silently winning instead
   (a real bug this order fixes — see MEMORY.md). */
.ql-manual-figure--float-left {
  float: left;
  margin-right: 1.5rem;
  max-width: 45%;
}

.ql-manual-figure--float-right {
  float: right;
  margin-left: 1.5rem;
  max-width: 45%;
}

.ql-manual-figure--size-small {
  max-width: 280px;
}

.ql-manual-figure--size-medium {
  max-width: 420px;
}

.ql-manual-figure--size-large {
  max-width: 640px;
}

.ql-manual-figure img {
  display: block;
  width: 100%;
  height: auto;
}

.ql-manual-figure figcaption {
  margin-top: 0.6rem;
  font-family: 'Inter', sans-serif;
  font-size: 0.85rem;
  color: var(--ql-muted);
  text-align: center;
}

/* Real bug caught in the mobile adaptation's final cross-page sweep
   (2026-07-18): `width: 100%` on a table is only a minimum suggestion
   under the default table-layout:auto, not a hard cap — the Criteria
   column's longer cell text ("Protein 15-35%, Carbs 40-55%, Fat 20-30%")
   forced the table wider than a phone's viewport regardless, overflowing
   the whole page rather than just this element. Wrapping it in a
   horizontally-scrollable container contains that overflow to the table
   itself; harmless at desktop width, where it never actually needs to
   scroll. */
.ql-manual-table-wrap {
  overflow-x: auto;
}

/* Reference tables (currently just the scoring section's point tables) —
   same dotted-row/cyan-header language as .ql-monthly-review-table, but in
   Inter (matches this page's body text) instead of VT323, and no reverse-
   video row hover since these rows aren't clickable/selectable like a
   records list. */
.ql-manual-table {
  width: 100%;
  margin: 1rem 0 1.5rem;
  border-collapse: collapse;
  font-family: 'Inter', sans-serif;
  font-size: 1rem;
}

.ql-manual-table th,
.ql-manual-table td {
  padding: 0.5rem 0.75rem;
  text-align: left;
  border-bottom: 1px dotted rgba(57, 255, 138, 0.3);
}

.ql-manual-table th {
  font-family: 'VT323', monospace;
  font-size: 0.95rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  border-bottom: 1px solid var(--ql-accent);
}

.ql-manual-table td:not(:first-child) {
  font-variant-numeric: tabular-nums;
}

.ql-manual-table tbody tr:last-child td {
  border-bottom: none;
}

/* Clearfix so a floated figure's height is contained within its own
   section (never bleeds into the next one), plus every heading clears
   floats unconditionally as a second line of defense. Scoped to the
   Manual page only — every other page's .ql-plain-section never floats
   anything, so this rule has no reason to apply there. */
body.ql-manual-page .ql-plain-section::after {
  content: '';
  display: table;
  clear: both;
}

body.ql-manual-page .ql-plain-section .ql-section-title {
  clear: both;
}

/* Temporary in-browser layout editor (2026-07-13, expanded same day,
   ?edit=1 only — see manual.js). Hidden by default; only ever added to
   the DOM at all when editModeEnabled, and body.ql-manual-editing
   (toggled by the "Hide/Show Edit Controls" button) further hides it
   without discarding the underlying saved edits. Not part of the page
   real staff see. */
.ql-manual-figure__toolbar {
  display: none;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.6rem;
}

body.ql-manual-editing .ql-manual-figure__toolbar {
  display: flex;
}

.ql-manual-figure__toolbar button,
.ql-manual-figure__toolbar select {
  font-family: 'Inter', sans-serif;
  font-size: 0.85rem;
  padding: 0.2rem 0.55rem;
  background: var(--ql-bg);
  border: 1px solid var(--ql-accent);
  color: var(--ql-text);
  cursor: pointer;
}

.ql-manual-figure__toolbar button:hover {
  background: var(--ql-accent);
  color: var(--ql-bg);
}

/* The file input itself stays invisible — its Replace button (a regular
   toolbar button) triggers it via .click(), same pattern as any custom
   "styled file upload" button. */
.ql-manual-figure__file-input {
  display: none;
}

#ql-manual-edit-toggle,
#ql-manual-edit-reset,
#ql-manual-edit-show {
  font-size: 0.95rem;
}

/* "Show My Changes" overlay — the handoff point for getting edits out of
   this browser's localStorage and into chat, since Claude has no other
   way to read them. Deliberately plain (no glow/animation): its only job
   is to be easy to read and copy from. */
.ql-manual-changes-overlay {
  position: fixed;
  inset: 0;
  z-index: 1002;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.75);
}

.ql-manual-changes-panel {
  width: min(700px, 90vw);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.5rem;
  background: var(--ql-panel);
  border: 1px solid var(--ql-accent);
}

.ql-manual-changes-panel h2 {
  font-family: 'VT323', monospace;
  font-size: 1.3rem;
  color: var(--ql-accent);
  margin: 0;
}

.ql-manual-changes-panel p {
  font-family: 'Inter', sans-serif;
  font-size: 0.95rem;
  color: var(--ql-muted);
  margin: 0;
}

.ql-manual-changes-textarea {
  flex: 1;
  min-height: 260px;
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 0.8rem;
  padding: 0.75rem;
  background: var(--ql-bg);
  color: var(--ql-text);
  border: 1px solid rgba(57, 255, 138, 0.35);
  resize: vertical;
}

.ql-manual-changes-actions {
  display: flex;
  gap: 0.75rem;
}

/* Text editing: every heading/paragraph/list directly inside a
   .ql-plain-section becomes contenteditable in edit mode (figures are
   excluded — they have their own toolbar instead). A dashed outline is
   the only visual cue, appearing on focus so it doesn't clutter a page
   that's otherwise supposed to preview close to the real thing. */
body.ql-manual-editing .ql-manual-editable-text:focus {
  outline: 1px dashed var(--ql-accent-cyan);
  outline-offset: 4px;
}

/* ==========================================================================
   Mobile adaptation (2026-07-18), Milestone 1 — foundation + Today's Quest.
   Staff need to log meals/power-ups/obstacles from a phone/tablet in the
   field, reversing this app's prior desktop-only decision (PRODUCT.md/
   DESIGN.md) on direct instruction. Additive breakpoint layered on top of
   the existing desktop-first rules, not a mobile-first rewrite — the same
   technique this file already used once, narrowly, for the trend chart
   column at 900px. 768px catches phones and portrait tablets.
   ========================================================================== */
@media (max-width: 768px) {
  /* Defensive: a sub-2px flex shrink-to-fit rounding artifact on .ql-page
     (confirmed absent at desktop widths, no actual content is clipped —
     just an imperceptible sliver of empty box edge) can otherwise open a
     real horizontal scroll gesture on touch devices. Needs to be on both
     html and body — the root scrolling element differs by browser, and
     setting it on body alone left document.documentElement.scrollWidth
     unaffected when this was actually tested live. */
  html {
    overflow-x: hidden;
  }

  body {
    padding: 1.5rem 0.75rem;
    overflow-x: hidden;
  }

  h1,
  .ql-heading {
    padding: 0 1rem;
  }

  /* Today's Quest's two-column console and the 3-wide log grid both
     collapse to a single stacked column. Every divider/padding rule below
     assumes side-by-side columns on desktop (see their own comments up
     near .ql-layout/.ql-log-grid's definitions) — reset here rather than
     just shrunk, since a vertical divider or a zeroed outer-edge padding
     stops making sense once there's no side to divide. */
  .ql-layout {
    grid-template-columns: 1fr;
    padding: 0 1rem;
  }

  /* Divider between the stats column and Meals & Snacks below it, once
     they stack on mobile (2026-07-22 direct request) — same dotted-line
     language as .ql-log-grid .ql-section's own divider further down, so
     the two stacked seams read consistently rather than one being a bare
     margin gap and the other an actual line.
     Switched to a double line (2026-07-23 direct follow-up) — needs at
     least 3px of width for the browser's `double` border-style to
     actually render as two lines rather than collapsing back to what
     looks like one. */
  .ql-column--quest {
    padding-right: 0;
    padding-bottom: 1.5rem;
    margin-bottom: 1.5rem;
    border-bottom: 3px double rgba(57, 255, 138, 0.35);
  }

  /* Real gap flagged live between the Health stat bar and [ Log Out ] —
     the desktop margin-top (1.75rem) reads fine there but left too much
     dead space once the whole quest column is a narrow mobile stack.
     Mobile-only: desktop's own spacing wasn't reported as an issue. */
  .ql-logout {
    margin-top: 0.5rem;
  }

  .ql-column--log {
    padding-left: 0;
    border-left: none;
    /* Lets .ql-boss-watch reorder ahead of .ql-log-grid below (see its own
       comment) — harmless for every other child, which already stacked
       in this same top-to-bottom DOM order as plain block content. */
    display: flex;
    flex-direction: column;
  }

  .ql-log-grid,
  .ql-log-grid.ql-profile-grid {
    grid-template-columns: 1fr;
  }

  /* Current Mission widget (Direction C, 2026-07-23) — a real bug
     reported live: this widget never got a mobile layout at all, so at
     phone widths it just inherited the desktop flex row as-is, which had
     no wrap plan and squeezed each piece into wrapping mid-phrase
     ("...LVL" / "1", "10 /" / "14") instead of the row adapting. Also
     sat after all 3 stacked Meals/Power-Ups/Obstacles sections, far down
     the page. Fixed two ways, both mobile-only (desktop's version, which
     already reads fine at that width, is untouched):
     1. Moved to the top of .ql-column--log via order:-1 (which, once
        .ql-column--quest and .ql-column--log stack into one column, puts
        it directly under the stat bars) instead of moving it in the DOM
        — the widget's real markup position stays after .ql-log-grid so
        desktop, which never runs this media query, is unaffected.
     2. Restyled into a compact single-line summary — name+level (already
        one combined string from app.js, ellipsis-truncated here in case
        a future boss name is long) and health value on one row, ahead of
        the mission line, which is demoted to a smaller, quieter line
        (no glow, no `>` prefix) rather than the desktop's loud
        section-heading treatment — a glance-and-go version, not the full
        detail. */
  .ql-boss-watch {
    order: -1;
    margin-top: 0;
    display: flex;
    flex-direction: column;
    /* Same 0.25rem as .ql-log-grid .ql-section above, so the widget's own
       left/right edge lines up with Meals & Snacks/Power-Ups/Obstacles
       (it has no other horizontal padding of its own — it sits flush
       with .ql-column--log otherwise). */
    padding-left: 0.25rem;
    padding-right: 0.25rem;
  }

  .ql-boss-watch__row {
    order: 0;
    min-width: 0;
  }

  .ql-boss-watch__name {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .ql-boss-watch__value {
    flex-shrink: 0;
  }

  /* Only the static "Current Mission:" label is demoted (muted, no glow,
     no `>` prefix) — .ql-boss-watch__mission-text (the dynamic part, e.g.
     "5 glasses of water") deliberately keeps its own unconditional
     var(--ql-text) color from its base rule above, unmuted, so the
     actual mission stays legible. An earlier pass muted this span too,
     which read wrong against the mockup and was flagged live — removed
     rather than overridden back, since there was never a reason for it
     to be quieter than the label introducing it. */
  .ql-boss-watch h2.ql-section-heading {
    order: 1;
    font-size: 0.8rem;
    color: var(--ql-muted);
    text-shadow: none;
    margin: 0 0 0.5rem;
  }

  .ql-boss-watch h2.ql-section-heading::before {
    content: none;
  }

  #ql-boss-watch-health {
    order: 2;
  }

  .ql-boss-watch__log {
    order: 3;
  }

  /* Missing divider, flagged live: .ql-log-grid .ql-section:not(:first-
     child) draws the app's standard divider between stacked sections,
     but that's a DOM-order selector — Meals & Snacks is still the DOM
     (and CSS :first-child) first section even though .ql-boss-watch now
     visually renders before it via order: -1, so it never gets a divider
     of its own, leaving a bare gap where one used to read consistently.
     Drawing the same divider as the *bottom* edge of the widget itself
     (rather than trying to target Meals & Snacks from a selector that
     can't see visual/flex order) sidesteps that mismatch entirely. */
  .ql-boss-watch {
    padding-bottom: 1.25rem;
    margin-bottom: 1.25rem;
    /* Double line (2026-07-23 direct follow-up), matching the
       .ql-column--quest divider above — 3px is the minimum width for
       `double` to actually render as two lines. */
    border-bottom: 3px double rgba(57, 255, 138, 0.35);
  }

  /* Same specificity as the desktop :first-child/:last-child rules that
     zero this padding (see .ql-log-grid's own comment) — must match
     exactly to win on source order rather than silently losing to them.
     Reduced from 1.25rem to 0.25rem (2026-07-23 direct request, "make
     the margin from the mission widget to Obstacles all the same as
     Daily Records, Monthly Review, and How This Works") — 0.25rem is the
     exact value .ql-history (Daily Records + Monthly Review's shared
     panel) and #ql-how-it-works already use for their own mobile
     left/right padding. */
  .ql-log-grid .ql-section:first-child,
  .ql-log-grid .ql-section:last-child {
    padding-left: 0.25rem;
    padding-right: 0.25rem;
  }

  /* The middle section (Power-Ups) had no left/right override at all
     before this — it inherited the desktop base .ql-section's 1.25rem
     unchanged, while the rule above was already restoring *some* padding
     specifically for the first/last sections (whose desktop padding is
     zeroed at those edges for the 3-column grid gutters). Same 0.25rem
     value here so all 3 sections end up flush with each other. */
  .ql-log-grid .ql-section {
    padding-left: 0.25rem;
    padding-right: 0.25rem;
  }

  .ql-log-grid .ql-section:not(:first-child) {
    border-left: none;
    border-top: 1px solid rgba(57, 255, 138, 0.35);
    padding-top: 1.25rem;
    margin-top: 1.25rem;
  }

  /* Profile/Boss Battle (2026-07-23 direct follow-up to the How This
     Works/Daily Records margin fix) — the unboxed panel still kept its
     desktop width:90%, and .ql-page-content added its own 1.75rem padding
     on top of that same double-inset problem. Narrowed the panel to 85%
     (matching How This Works/Daily Records) and reduced the inner
     padding, scoped to body.ql-unboxed so index.html's own .ql-panel--wide
     (still boxed via Direction C's column layout, different needs) is
     unaffected.
     Retargeted from .ql-panel--wide to .ql-hud-page-wrap (2026-07-23,
     same day — Direction B's HUD frame shipped after this rule was
     written): .ql-panel--wide is now .ql-hud-page-wrap's *inner* child,
     narrowed to 85% of its own already-90%-narrowed-on-desktop-only wrap
     while the wrap itself stayed unnarrowed on mobile — the wrap's own
     green gradient background showed through the ~50px gap as a solid
     colored column down the right edge, a real bug reported live. Since
     .ql-unboxed + .ql-panel--wide only ever appears paired with
     .ql-hud-page now (both pages that use one use the other), narrowing
     the true outer element and leaving .ql-hud-page's own unconditional
     width:100% (see its definition) fill that narrowed wrap is the
     correct fix, not stacking a second override on the inner panel. */
  body.ql-unboxed .ql-hud-page-wrap {
    width: 87%;
  }

  /* .ql-hud-page-label (the "01 · PROFILE"/"01 · BOSS BATTLE" caption
     outside the frame) needs its own matching width here — it was left
     at the desktop-scoped rule's value while the wrap above got narrowed
     for mobile, so the label's left edge no longer lined up with the
     frame's own left edge. Flagged live alongside the same request that
     widened the desktop wrap from 90% to 92% (see that rule's own
     comment) — this mobile pairing was missed in that same pass. */
  .ql-hud-page-label {
    width: 87%;
  }

  .ql-page-content {
    padding: 0 0.25rem 0.5rem;
  }

  /* Data Export / Settings — same margin fix, one level up: these pages
     never sat inside a panel to begin with (.ql-plain-page is a direct
     .ql-page child), but still carried the desktop width:80% unchanged
     onto mobile with no padding compensation. Excludes the Manual page,
     which already has its own tailored mobile treatment (full width, 1rem
     margin, TOC drawer) further down this file. */
  body.ql-no-page-scanline:not(.ql-manual-page) .ql-plain-page {
    width: 85%;
  }

  /* Boss Battle's Attack & Defence / How to Defeat / Cheat Code paragraphs
     — same treatment as How This Works' body copy above (2026-07-23
     direct request): font-size down from the inherited 20px to 1.05rem,
     line-height tightened from the desktop 1.6 to 1.4, now that the
     unboxed panel above is also narrower on mobile. */
  #ql-boss-attack-text,
  #ql-boss-defence-text,
  #ql-boss-howto-text,
  #ql-boss-cheat-text {
    font-size: 1.05rem;
    line-height: 1.4;
  }

  /* Real gap found live (user report: "the margin looks way bigger than
     How This Works"): .ql-section's base padding:0 1.25rem (built for
     desktop grid-column gutters) never gets zeroed on mobile the way
     :first-child/:last-child are on desktop — the existing mobile rule
     even restores 1.25rem on those two specifically, so every stacked
     section (first, middle, last) carried a full 20px of horizontal
     padding on top of the panel/page-content insets already narrowed
     above. How This Works has no such per-section padding layer at all,
     which is why the two read so differently. Scoped to Profile/Boss
     (body.ql-unboxed) — same specificity (3 classes) as the existing
     :first-child/:last-child mobile rule, wins on source order since it
     comes later, and index.html's own log-grid (Meals/Power-ups/
     Obstacles) is left untouched. */
  body.ql-unboxed .ql-log-grid .ql-section {
    padding-left: 0.25rem;
    padding-right: 0.25rem;
  }

  /* Mobile header stays one row (name left, switcher right) — same as
     desktop, since the nav below is now a compact toggle+dropdown instead
     of 3 wrapping buttons. Needs its own positioning context for #ql-nav's
     dropdown (below) to anchor against. */
  .ql-titlebar--with-nav {
    position: relative;
    flex-wrap: nowrap;
  }

  /* Direction B's chamfer-clearance padding-left (2.5rem, desktop) ate
     into the already-tight margin here — reverted to the base titlebar's
     1.5rem on mobile, since the chamfer's clearance matters far less than
     keeping the row from overflowing. */
  .ql-hud-page .ql-titlebar {
    padding-left: 1.5rem;
  }

  /* Real bug reported live, still present after the padding-left revert
     above: "QuestLog" + the cursor wrapped onto their own second line
     inside .ql-titlebar__title's flex item. Root cause wasn't the
     padding — it's .ql-titlebar--with-nav's own gap: 24px between the
     title and the page-switcher toggle, which this title span's default
     flex-shrink: 1 absorbs first whenever the row is a few px too tight
     (measured ~2px short at 375px with the "Boss Battle" switcher label,
     the longer "Today's Quest" label on index.html is at equal risk).
     Since .ql-titlebar__title has no white-space of its own, the browser
     wraps its two inline children (name span, cursor span) onto separate
     lines rather than visibly overflowing. Un-shrinkable + non-wrapping
     forces the row to overflow by that same couple of px instead, already
     silently clipped by this same block's html/body overflow-x: hidden —
     invisible, and far better than a broken 2-line title. Not scoped to
     .ql-hud-page since the same gap/switcher combination exists on
     Today's Quest too. */
  .ql-titlebar__title {
    flex-shrink: 0;
    white-space: nowrap;
  }

  /* Compact page-switcher trigger (2026-07-22 direct request, several
     rounds of visual iteration) — plain text, no border box, current
     page's short label + a muted caret. Tapping toggles #ql-nav open
     (topbar.js's initPageNav()); each page's own script sets this
     button's label directly (app.js/profile.js/boss.js). */
  .ql-nav-toggle {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    gap: 0.3rem;
    background: none;
    border: none;
    padding: 0.35rem 0;
    font-family: 'VT323', monospace;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ql-accent);
    text-shadow: 0 0 2px rgba(57, 255, 138, 0.6);
    white-space: nowrap;
    cursor: pointer;
  }

  .ql-nav-toggle::before {
    content: '[ ';
  }

  .ql-nav-toggle::after {
    content: ' ] \25BE';
    color: var(--ql-muted);
    text-shadow: none;
  }

  /* #ql-nav itself becomes the dropdown (2026-07-22 direct request,
     several rounds of iteration) — hidden until .is-open, then all 3
     links show at once, current one already reverse-video via the shared
     [aria-current="page"] rule above (no extra override needed). Panel-
     surface fill (approved from a background comparison mockup — a flat
     void fill read as "no fill at all," a green→black gradient was too
     heavy for an everyday control). Lifted above the CRT scanline overlay
     (body::before, z-index:999) the same way How This Works/Contact/the
     login fields already are, otherwise its lines would paint over the
     open dropdown's text. */
  .ql-nav {
    display: none;
    flex-direction: column;
    position: absolute;
    top: 100%;
    right: 1.5rem;
    z-index: 1000;
    width: max-content;
    min-width: 140px;
    background: var(--ql-panel);
    border: 1px solid var(--ql-accent);
  }

  .ql-nav.is-open {
    display: flex;
  }

  /* Explicit line-height (not the inherited "normal") so all 3 rows
     compute to the exact same height regardless of any per-row font-
     metric drift — the reverse-video current row previously read taller
     than the other two. */
  .ql-nav a.ql-button {
    display: block;
    margin: 0;
    padding: 0.3rem 0.75rem;
    line-height: 1.2;
    color: var(--ql-muted);
    text-shadow: none;
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
  }

  .ql-nav a.ql-button::before {
    content: '> ';
    color: rgba(57, 255, 138, 0.4);
  }

  .ql-nav a.ql-button::after {
    content: none;
  }

  .ql-nav a.ql-button:hover {
    background: var(--ql-accent);
    color: var(--ql-bg);
  }

  .ql-nav a.ql-button:hover::before {
    color: var(--ql-bg);
  }

  /* .ql-nav a.ql-button's own color:var(--ql-muted) above (0,2,1
     specificity) would otherwise beat the shared, non-mobile-scoped
     .ql-button[aria-current='page'] rule (0,2,0) that gives the current
     page its reverse-video fill everywhere else — restored explicitly
     here so the current row still reads as dark-on-green in this
     dropdown, not muted-on-green. */
  .ql-nav a.ql-button[aria-current='page'] {
    color: var(--ql-bg);
  }

  .ql-nav a.ql-button[aria-current='page']::before {
    color: var(--ql-bg);
  }

  /* How This Works / Daily Records / Monthly Review all inherited their
     desktop width:90%+1.75rem-padding as-is on mobile (2026-07-23 direct
     request/report: "the whole page still looks a bit restricted"). At a
     375px viewport that's ~34px of margin plus 28px of padding per side —
     over 90px of the 375px total spent on whitespace before any text
     starts, on top of body's own 0.75rem page gutter. Narrowed to 85%
     (down from 90%) with a small padding here (relying on body's gutter
     for most of the outer margin instead of stacking a second one on top
     of it), closer to the reference's tight mobile text columns. */
  #ql-how-it-works {
    width: 85%;
    padding: 0 0.25rem;
  }

  .ql-history {
    width: 85%;
    padding: 0 0.25rem 1.5rem;
  }

  /* Body copy shrunk to fit more per line in the freed-up width above —
     was 1.3rem (~20.8px) everywhere, sized for the desktop 2-column grid's
     wide column; the mobile single column already reduces the heading and
     pull-quote (below) but had left the actual paragraph text untouched.
     Line-height also tightened from the desktop 1.6 (generous for a wide
     52ch column) to 1.4, closer to the reference's denser paragraph
     rhythm now that lines are naturally shorter on a narrow screen. */
  .ql-how-it-works__support,
  .ql-how-it-works__stack p {
    font-size: 1.05rem;
    line-height: 1.4;
  }

  /* Daily Records rows and Monthly Review's table cells inherit body's
     full 20px with fairly generous padding — same "restricted" density
     issue as the paragraphs above, just for tabular/list data instead of
     prose. Tightened to match. */
  .ql-history-item {
    padding: 0.5rem 0.25rem;
    font-size: 1.05rem;
  }

  .ql-monthly-review-table th,
  .ql-monthly-review-table td {
    padding: 0.4rem 0.4rem;
  }

  /* Just the data cells — .ql-monthly-review-table th already has its own
     smaller, deliberately distinct label size (0.85rem) that this
     shouldn't touch. */
  .ql-monthly-review-table td {
    font-size: 0.95rem;
  }

  /* "How This Works" heading + "Read the full Staff Manual ->" link
     (index.html): the link has flex-shrink:0 by design (it must never
     shrink its own text to fit), so on a narrow screen the pair no longer
     fits on one line — wraps instead of pushing the link off the right
     edge of the viewport. */
  .ql-how-it-works__header {
    flex-wrap: wrap;
  }

  /* Real bug found live 2026-07-21: .ql-how-it-works-grid's desktop 2-column
     grid (1.1fr 1fr) never collapsed on mobile, so each column squeezed to
     ~150px and nearly every line wrapped one or two words at a time. Single
     column, heading drops from the desktop-only 2.6rem, and the pull-quote
     matches the support paragraphs' own size (direct request — it no longer
     has a wide column to stand out in). The divider replaces the lost grid
     `gap` so the pull-quote block and the "Staff run the log" block still
     read as two distinct groups instead of running together. */
  .ql-how-it-works-grid {
    grid-template-columns: 1fr;
    gap: 0;
  }

  #ql-how-it-works-heading.ql-manual-heading {
    font-size: 1.9rem;
  }

  .ql-how-it-works__pull {
    font-size: 1.3rem;
  }

  .ql-how-it-works__stack {
    margin-top: 1.25rem;
    padding-top: 1.25rem;
    border-top: 1px solid rgba(57, 255, 138, 0.35);
  }

  /* Same bug, Contact section: .ql-contact-row's desktop side-by-side flex
     row (title + body sharing one line, gap: 3.5rem) left the body
     paragraph squeezed into a sliver once the title's own 3rem font ate
     most of the width — stack instead. */
  .ql-contact-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }

  .ql-contact-row__title {
    font-size: 2.2rem;
  }

  /* Keeps the phone-sized contact form as originally tested/approved — the
     2026-07-21 desktop-only font bump (1.1rem -> 1.2rem, label width
     6.5rem -> 7rem) was a direct request scoped to desktop specifically,
     not the already-approved mobile sizing. */
  .ql-contact-form__field label {
    width: 6.5rem;
    font-size: 1.1rem;
  }

  .ql-contact-form__field input,
  .ql-contact-form__field textarea {
    font-size: 1.1rem;
  }

  .ql-contact-form__submit {
    font-size: 1.1rem;
  }

  /* Topbar: 5+ links (language toggle, How This Works, Manual, Data
     dropdown, Contact) have no room inline on a phone, so they collapse
     into a toggleable drawer instead — see topbar.js for the open/close
     wiring shared by every page that includes this bar. */
  .ql-topbar {
    padding: 1rem 1.25rem;
  }

  .ql-topbar__toggle {
    display: inline-block;
  }

  .ql-topbar__links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--ql-panel);
    border-bottom: 1px solid var(--ql-accent);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
    padding: 0.5rem 0;
  }

  .ql-topbar__links.is-open {
    display: flex;
  }

  /* .ql-topbar__lang and .ql-topbar__link--data are both <button>s, which
     center their own text by default — every other link here is an <a>,
     which doesn't. Left-align explicitly so the whole drawer reads as one
     aligned list instead of some items drifting to the center. */
  .ql-topbar__links .ql-topbar__lang,
  .ql-topbar__links .ql-topbar__link {
    width: 100%;
    padding: 0.85rem 1.25rem;
    text-align: left;
  }

  /* The Data submenu flattens into the open drawer instead of staying a
     hover-flyout — there's no hover on touch, and a nested flyout inside
     an already-open drawer is one interaction too many (mobile guidance:
     reduce navigation complexity, don't rely on hover for functionality). */
  .ql-topbar__dropdown {
    width: 100%;
  }

  /* "Data" itself stops looking like a third clickable link once its two
     children are always-visible rows below it rather than a hover flyout —
     it's a group label now, not a dead-end button. Reuses the Manual
     sidebar's own group-label treatment (small, muted, tracked caps) for
     the same visual language, rather than inventing a second one. The
     hover-sweep pseudo-elements (::before/::after, inherited from
     .ql-topbar__link) are hidden since a group label isn't interactive. */
  .ql-topbar__links .ql-topbar__link--data {
    padding-bottom: 0.3rem;
    letter-spacing: 0.13em;
    color: var(--ql-muted);
    text-shadow: none;
    cursor: default;
    pointer-events: none;
  }

  .ql-topbar__links .ql-topbar__link--data::before {
    content: none;
  }

  .ql-topbar__menu {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    box-shadow: none;
    border: none;
    padding: 0;
    min-width: 0;
  }

  /* Indented under the "Data" group label, and matching every other link's
     dim-at-rest/bright-on-hover treatment (.ql-topbar__link) instead of
     the desktop flyout's always-bright color — a submenu item shouldn't
     visually outrank the top-level links around it. */
  .ql-topbar__menu-link {
    padding: 0.5rem 1.25rem 0.85rem 2.5rem;
    color: rgba(216, 245, 232, 0.5);
    background: transparent;
  }

  .ql-topbar__menu-link:hover,
  .ql-topbar__menu-link:focus-visible {
    background: transparent;
    color: var(--ql-accent);
    text-shadow:
      0 0 2px var(--ql-accent),
      0 0 8px rgba(57, 255, 138, 0.7);
  }

  /* Milestone 2 — Profile's character/avatar picker panel. Desktop opens it
     600px wide, anchored 1.5rem to the *right* of its trigger via
     position:absolute — on any mobile viewport that would render almost
     entirely off-screen. Switches to a fixed, near-full-screen overlay
     instead of scaling the same layout down; same trigger/preview/select
     interaction and existing Escape/outside-click-closes JS, no script
     changes needed.
     Revised 2026-07-28 (`/impeccable adapt`) — the first pass (centered via
     top:50%/transform + max-height: calc(100dvh - 4rem)) still cut off the
     stage-tiles row behind the browser's own toolbar on a real phone, even
     after switching from vh to dvh. Root cause wasn't just the vh/dvh
     choice — top:50%/transform:translate(-50%,-50%) plus a computed
     max-height is still fragile against exactly the dynamic-toolbar
     resize/repaint quirks a real mobile browser has that desktop dev-tools
     emulation doesn't reproduce. Switched to `inset` instead: a
     position:fixed element's inset is resolved against the browser's
     actual current visual viewport, not a vh-style "largest possible"
     reference — no centering math, no vh/dvh fallback chain needed at
     all. This also gives the content meaningfully more vertical room
     (fewer, shorter scrolls) rather than fighting a fixed max-width/height
     box for space. */
  .ql-avatar-picker__panel {
    position: fixed;
    /* top/bottom (not inset's shorthand) so the panel can still be capped
       narrower than the viewport width and centered horizontally — direct
       follow-up request to make the window smaller. inset:0.75rem sets
       left+right both explicitly, which is over-constrained against an
       explicit width (tried a max-width + margin:auto combo first; it
       produced a negative computed margin and a shrink-to-fit box wider
       than the viewport instead of the intended capped/centered one).
       left:50%/transform:translateX(-50%) is the standard, non-conflicting
       way to center a capped-width fixed box — safe here (unlike the
       .ql-hud-page-wrap/.ql-hud-page transform regression) since this
       transform is on the panel itself, not an ancestor of anything else
       relying on position:fixed against the viewport.
       width via the max-width *property* (not the min() *function*) —
       real bug caught live on the real device (confirmed via a debug
       banner that the phone genuinely was on fresh code, ruling out any
       caching explanation): `width: min(calc(100% - 1.5rem), 360px)`
       rendered as if this whole rule's width were never applied at all,
       falling back to the desktop base rule's `width: 600px` — on a
       393px-wide phone that overflows off-screen on both sides, which is
       exactly why it looked "full width with no margin" (only the middle
       slice of an oversized, off-center-appearing box was ever visible).
       `max-width` is a plain CSS2.1 property, not a CSS Values 4 math
       function — far more conservative and long-proven across engines. */
    top: 0.75rem;
    bottom: 0.75rem;
    left: 50%;
    transform: translateX(-50%);
    width: calc(100% - 1.5rem);
    max-width: 360px;
    overflow-y: auto;
    /* Desktop's z-index:20 (base rule) only ever had to clear adjacent
       page content, not the fixed topbar (1001) — real bug caught live:
       the panel's top edge rendered underneath the topbar instead of over
       it. Matches this app's own elevation scale (see DESIGN.md/the
       .ql-topbar comment): full-screen modals sit at 1002, above the
       topbar, and this panel is functionally a modal now that it's a
       fixed, near-full-screen overlay on mobile. */
    z-index: 1002;
  }

  /* Grid + preview no longer fit side by side at this width — stacks
     instead, grid first (matches the approved mockup's own reading order:
     browse, then see the live preview). */
  .ql-avatar-picker__body {
    flex-direction: column;
    gap: 1.5rem;
  }

  .ql-avatar-picker__preview {
    border-left: none;
    border-top: 1px dotted rgba(57, 255, 138, 0.35);
    padding-left: 0;
    padding-top: 1.5rem;
  }

  /* Milestone 3 — the Manual's fixed left sidebar Table of Contents
     (220px, position:fixed) has no room to stay permanently visible
     alongside the content column on a phone. Collapses behind a toggle
     instead of shrinking in place, same "drawer, not a squeezed sidebar"
     call as the topbar. Closed by default (no .is-open class); becomes a
     full-viewport overlay when opened rather than a partial slide-in
     drawer, since a 220px sidebar has nothing useful to show alongside on
     a narrow screen anyway. z-index:1002 for the same reason the avatar
     picker panel needed it above — this is functionally a modal now. */
  .ql-manual-toc-toggle {
    display: inline-block;
  }

  .ql-manual-toc-close {
    display: inline-block;
  }

  .ql-manual-sidebar {
    display: none;
    position: fixed;
    inset: 0;
    width: auto;
    max-width: none;
    top: 5.5rem;
    padding: 1.5rem;
    z-index: 1002;
    border-right: none;
  }

  .ql-manual-sidebar.is-open {
    display: block;
  }

  /* The sidebar is out of flow either way (fixed, hidden by default), so
     the content column just needs full-width clearance instead of the
     260px left margin desktop reserves for the always-visible sidebar.
     Real bug caught live: `width: auto` alone doesn't mean "fill available
     space" here — .ql-plain-page is a flex item of .ql-page, whose
     align-items:center makes width:auto resolve via shrink-to-fit against
     the content's own preferred size, not the viewport. Removing max-width
     let that shrink-to-fit width grow wider than the screen with nothing
     bounding it, clipping text at both edges once overflow-x:hidden (see
     the Milestone 1 entry) hid the resulting horizontal scroll. Same fix
     as .ql-page's own identical bug elsewhere in this file: align-self:
     stretch gives it a real, definite width instead of guessing one. */
  body.ql-manual-page .ql-plain-page {
    width: auto;
    max-width: none;
    align-self: stretch;
    margin: 1.5rem 1rem 2rem;
  }

  /* Milestone 4 — Data Export's Activity Breakdown. Desktop's fixed
     2-column grid (240px minimum each) demands ~500px, which doesn't fit
     a phone's content width (~280-320px) — real bug caught live: the
     second column rendered fully off-screen to the right, invisible
     rather than scrollable, since Milestone 1's defensive
     overflow-x:hidden clips rather than reveals a scrollbar for exactly
     this class of overflow. Collapses to a single column instead;
     Obstacles Logged (the 3rd child) still lines up in the same track
     width as Meals/Power-Ups above it either way, since that guarantee
     came from all 3 sharing one grid, not from the 2-column count itself. */
  .ql-breakdown-groups {
    grid-template-columns: 1fr;
  }
}

/* Printable/PDF version — triggered by the Export as PDF button
   (window.print()). A deliberate, scoped exception to the One-Voice Rule
   (VT323-only typography) and the dark terminal palette: a small pixel
   font and glowing dark-mode text are hard to read on paper, so the
   print output switches to a plain system sans-serif on a white
   background instead. This is a print-ergonomics call specific to this
   one page's PDF/handout use case, not a change to the app's on-screen
   visual identity — every other page is untouched, and this page's own
   screen version keeps the terminal look. */
@media print {
  body.ql-manual-page {
    padding-top: 0;
    background: #fff;
    color: #111;
    font-family: -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  }

  body.ql-manual-page .ql-topbar,
  body.ql-manual-page #ql-manual-export,
  body.ql-manual-page #ql-manual-toc-toggle,
  body.ql-manual-page #ql-manual-toc-close,
  body.ql-manual-page .ql-manual-sidebar,
  body.ql-manual-page .ql-manual-figure__toolbar,
  body.ql-manual-page #ql-manual-edit-toggle,
  body.ql-manual-page #ql-manual-edit-reset,
  body.ql-manual-page #ql-manual-edit-show,
  body.ql-manual-page .ql-manual-changes-overlay {
    display: none;
  }

  body.ql-manual-page .ql-plain-page {
    width: 100%;
    max-width: none;
    margin: 0;
  }

  body.ql-manual-page .ql-heading,
  body.ql-manual-page .ql-section-title,
  body.ql-manual-page .ql-plain-section p,
  body.ql-manual-page .ql-plain-section li,
  body.ql-manual-page .ql-manual-table th,
  body.ql-manual-page .ql-manual-table td {
    color: #111;
    text-shadow: none;
    font-family: -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  }

  body.ql-manual-page .ql-manual-table {
    break-inside: avoid-page;
  }

  body.ql-manual-page .ql-manual-table th,
  body.ql-manual-page .ql-manual-table td {
    border-color: #ccc;
  }

  body.ql-manual-page .ql-section-title::before {
    border-top-color: #ccc;
  }

  body.ql-manual-page .ql-plain-section {
    break-inside: avoid-page;
  }

  body.ql-manual-page .ql-manual-figure {
    border-color: #ccc;
    background: transparent;
    break-inside: avoid-page;
  }

  body.ql-manual-page .ql-manual-figure figcaption {
    color: #555;
  }
}

.ql-history .ql-page-copy {
  padding: 0;
}

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* Onboarding Tour — first-login walkthrough on Today's Quest (2026-07-15).
   Reuses the site's documented z-index tiers (see Elevation in DESIGN.md):
   the scrim sits at 999, the same tier as the CRT scanline (body::before)
   — appended later in the DOM, so it paints above the scanline at equal
   z-index without needing a higher number. Each step's spotlighted
   target(s) rise to 1000 via .ql-onboarding-lift ("scanline-escaped
   content", the same tier How This Works and the login fields already
   use) so they stay legible above both the scanline and this scrim.

   The spotlight ring itself is a separate synthetic overlay
   (.ql-onboarding-spotlight-box), not an outline drawn directly on the
   target — a step can spotlight more than one element (Profile + Boss
   Battle, not the whole nav row including Today's Quest) as a single
   continuous box this way, positioned via JS from the union of their
   real bounding rects, with no shared wrapper needed in the real
   markup. */
.ql-onboarding-scrim {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: rgba(3, 13, 8, 0.75);
  pointer-events: none;
}

.ql-onboarding-lift {
  position: relative;
  z-index: 1000;
}

.ql-onboarding-spotlight-box {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  outline: 2px solid var(--ql-accent-cyan);
  outline-offset: 4px;
  box-shadow:
    0 0 0 4px var(--ql-bg),
    0 0 24px rgba(54, 255, 255, 0.35);
}

/* The topbar step's target (.ql-topbar) never receives .ql-onboarding-lift
   — it already sits at z-index:1001 (above every other tier, including
   this scrim) for its own documented reason (2026-07-14 fix), and adding
   a same-specificity z-index:1000 rule would silently lose to source
   order and demote it below the scrim, reopening that exact bug. This
   modifier only changes the box's own outline direction: it draws inward
   since the topbar sits flush against the viewport's top/left/right
   edges with no room to draw outward. */
/* z-index bumped above .ql-topbar's own 1001 — the base box's 1000 loses
   to the topbar's opaque background at equal-ish stacking (real bug,
   caught via user report: the ring rendered fully invisible on this step,
   painted underneath the topbar it was supposed to outline). Nothing else
   on the page needs to win against this while the tour is running. */
.ql-onboarding-spotlight-box--edge {
  z-index: 1002;
  outline-offset: -2px;
  box-shadow: 0 0 24px rgba(54, 255, 255, 0.35);
}

.ql-onboarding-callout {
  position: fixed;
  z-index: 1000;
  width: 300px;
  max-width: calc(100vw - 2rem);
  background: var(--ql-panel);
  border: 1px solid var(--ql-accent-cyan);
  box-shadow: 0 0 20px rgba(54, 255, 255, 0.25);
  padding: 0.9rem 1rem;
  text-align: left;
}

.ql-onboarding-callout__step {
  font-size: 0.9rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ql-accent-cyan);
  text-shadow: 0 0 4px rgba(54, 255, 255, 0.6);
  margin: 0 0 0.4rem;
}

.ql-onboarding-callout p {
  margin: 0 0 0.9rem;
  font-size: 1.05rem;
  line-height: 1.4;
  color: var(--ql-text);
}

.ql-onboarding-callout__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* Plain muted text, not a bracket button — Skip is a secondary escape
   hatch, not the primary action, so it doesn't compete with Next/Done. */
.ql-onboarding-callout__skip {
  background: transparent;
  border: none;
  padding: 0;
  font-family: 'VT323', monospace;
  font-size: 1rem;
  color: var(--ql-muted);
  cursor: pointer;
}

.ql-onboarding-callout__skip:hover,
.ql-onboarding-callout__skip:focus-visible {
  color: var(--ql-text);
}

.ql-onboarding-callout__advance {
  margin-top: 0;
}

.ql-onboarding-callout__advance--next::after {
  content: ' →';
}
