/* Shared design tokens + the small set of components that are byte-identical
   across generate.html, tracker.html, profile.html, auth.html, billing.html.
   Everything here used to be copy-pasted. Page-specific CSS stays inline per
   page. See DESIGN.md for the rules these values encode.

   Apple-system retrofit (2026-07-23): superseded below. Kept for history --
   the app briefly matched an approved "Find Jobs — Apple Design" reference
   artifact (generic blue/SF Pro), corroborated against music.apple.com.

   CVOWL brand palette (2026-07-28): the Apple retrofit was a placeholder
   before there was a real brand to point to. landing/index.html later broke
   from it deliberately -- warm paper, teal, editorial serif/mono, an owl
   mark -- and tested well. Running two design languages across the login
   boundary was the actual "generic SaaS" tell, not any one color choice.
   Values below are landing/index.html's own :root block, copied exactly (not
   re-derived) so the two files can't drift into two near-identical palettes
   again. --accent and --safe both resolve to the same teal as landing's
   --teal (one verified/brand color, not two); --flag matches landing's
   --amber. See the app redesign brief for the full reasoning. */

:root {
  --paper: #fbfaf7;
  --surface: #ffffff;
  --surface-tertiary: #f0efe6;
  --ink: #16161a;
  --ink-soft: #5b5b66;
  /* #86869a until a dark/light contrast audit (2026-07-28) measured it at
     3.57:1 on white -- under the 4.5:1 AA bar, and this token colors EVERY
     uppercase form label in the app (First name, Email, Company, ...).
     #6f6f82 keeps the same slight violet-grey bias but measures 4.92:1 on
     --surface and 4.71:1 on --paper. Dark mode's #7d7d8c already passed and
     is unchanged. */
  --ink-tertiary: #6f6f82;
  --ink-rgb: 22, 22, 26;
  --line: #e8e4da;
  /* Landing's brand teal is #0a8f7c. At small text sizes on a light ground
     that measures 3.84-4.01:1 -- under the 4.5:1 AA bar -- and it's used
     for eyebrows, badges, links, and button labels throughout the app.
     #087566 is the next step down in the same teal family (it was already
     in this palette as --accent-hover) and measures 5.37-5.60:1.
     Dark mode is untouched: #6ee7d8 on a near-black ground already passes
     comfortably, so only the light ramp shifts.

     TRADEOFF, FOUNDER'S CALL: this makes the app's light-mode teal a touch
     deeper than landing/index.html's, which is off-limits to edit and
     keeps #0a8f7c (and its own 3.84:1 eyebrows). Accessibility was taken
     as the tie-breaker over exact-shade parity. To revert to landing's
     exact teal, set --accent back to #0a8f7c and --accent-hover to
     #087566 -- one line each, nothing else depends on the change. */
  --accent: #087566;
  --accent-hover: #066052;
  --safe: #087566;
  --safe-soft: rgba(8, 117, 102, 0.1);
  /* Same reasoning as --accent: landing's --amber is #c1571f (4.33:1 on
     --paper). #a84a1a is the same hue, 5.50:1. */
  --flag: #a84a1a;
  --flag-soft: rgba(168, 74, 26, 0.1);
  /* Was #FF3B30 (Apple systemRed, left over from the pre-rebrand palette --
     landing has no danger color of its own to inherit). 3.55:1 on white,
     failing AA on the "failed" status badge and delete-account controls.
     #D70015 is the darker accessible red from the same family: 5.38:1. */
  --danger: #D70015;
  --danger-soft: rgba(215, 0, 21, 0.1);
  --info: #087566;
  --info-soft: rgba(8, 117, 102, 0.1);
  --shadow-card: 0 1px 3px rgba(22, 22, 26, 0.05);
  --shadow-modal: 0 10px 30px rgba(22, 22, 26, 0.09);
  --overlay-soft: rgba(22, 22, 26, 0.2);
  --overlay-medium: rgba(22, 22, 26, 0.35);
  /* Reserved gradient moment -- matches landing/index.html's --cta-gradient
     exactly. Used sparingly (one true "moment" per page, not a general
     accent), same discipline as landing: the primary CTA and one closing/
     celebratory state, never general decoration. */
  --cta-gradient: linear-gradient(135deg, #7b2ff7, #f72585);
  /* Ambient wash strength (see body::before/::after). Landing runs its
     hero blobs at 0.22/0.16 light and 0.55/0.32 dark; these are roughly a
     third of that -- landing is selling, the app is being worked in. */
  --wash-warm-op: 0.07;
  --wash-teal-op: 0.05;
  /* Third type role, editorial eyebrows/tags/metadata -- matches landing's
     --font-editorial. The app had no equivalent before this pass. */
  --font-editorial: ui-monospace, SF Mono, Menlo, Consolas, var(--mono);
  /* --serif was aliased to --sans ("Apple-system retrofit", 2026-07-23) on
     the theory that the effect would be identical either way -- it wasn't.
     Every app page already writes `font-family: var(--serif)` on its own
     headings, stat numbers, and drawer/modal titles (billing's
     .balance-number, tracker's .score-big/.stat-value, admin's
     .stat-card .stat-value, generate's .fit-report h3, auth's own
     .wordmark) clearly assuming a real display serif, matching the same
     serif the marketing landing page (landing/index.html's
     --font-display) uses for its headline/wordmark. Aliasing it to --sans
     silently flattened every one of those into plain system-ui, which is
     the actual reason the signed-in app reads as a flatter, more generic
     product than the landing page: it's not a missing accent, it's an
     accent every page already asked for that got quietly discarded here.
     --mono stays aliased to --sans -- labels that read as "system UI
     chrome" (EMAIL, PASSWORD, uppercase eyebrows) use the sans stack with
     letter-spacing, not a monospace face -- see the reference artifact's
     .stat-label / .company-name treatment. */
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --serif: Iowan Old Style, Palatino Linotype, Georgia, var(--sans);
  --mono: var(--sans);
  /* Brand teal for the owl mark -- deliberately still landing's exact
     #0a8f7c, NOT the darkened --accent above. This colors a graphic, not
     text, so it's held to the 3.0:1 non-text bar (which it clears), and
     the logo is the one element where matching landing pixel-for-pixel
     matters more than squeezing out extra text contrast. */
  --logo-teal: #0a8f7c;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #08080b;
    --surface: #101018;
    --surface-tertiary: #1c1c26;
    --ink: #FFFFFF;
    --ink-soft: #b9b9c4;
    --ink-tertiary: #7d7d8c;
    --ink-rgb: 255, 255, 255;
    --line: #23232e;
    --accent: #6ee7d8;
    --accent-hover: #8ff0e3;
    --safe: #6ee7d8;
    --safe-soft: rgba(110, 231, 216, 0.14);
    --flag: #ff8a4c;
    --flag-soft: rgba(255, 138, 76, 0.14);
    --danger: #FF453A;
    --danger-soft: rgba(255, 69, 58, 0.16);
    --info: #6ee7d8;
    --info-soft: rgba(110, 231, 216, 0.14);
    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-modal: 0 8px 30px rgba(0, 0, 0, 0.5);
    --overlay-soft: rgba(0, 0, 0, 0.35);
    --overlay-medium: rgba(0, 0, 0, 0.5);
    --logo-teal: #6ee7d8;
    --wash-warm-op: 0.16;
    --wash-teal-op: 0.11;
  }
}
:root[data-theme="dark"] {
  --paper: #08080b;
  --surface: #101018;
  --surface-tertiary: #1c1c26;
  --ink: #FFFFFF;
  --ink-soft: #b9b9c4;
  --ink-tertiary: #7d7d8c;
  --ink-rgb: 255, 255, 255;
  --line: #23232e;
  --accent: #6ee7d8;
  --accent-hover: #8ff0e3;
  --safe: #6ee7d8;
  --safe-soft: rgba(110, 231, 216, 0.14);
  --flag: #ff8a4c;
  --flag-soft: rgba(255, 138, 76, 0.14);
  --danger: #FF453A;
  --danger-soft: rgba(255, 69, 58, 0.16);
  --info: #6ee7d8;
  --info-soft: rgba(110, 231, 216, 0.14);
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-modal: 0 8px 30px rgba(0, 0, 0, 0.5);
  --overlay-soft: rgba(0, 0, 0, 0.35);
  --overlay-medium: rgba(0, 0, 0, 0.5);
  --logo-teal: #6ee7d8;
  --wash-warm-op: 0.16;
  --wash-teal-op: 0.11;
}

* { box-sizing: border-box; }

/* ---------- Ambient warmth (Cycle 3) ----------
   The founder's read: the app's palette matched landing but the product
   felt lifeless next to it, and the empty space was the tell. The audience
   makes this delicate -- people worn down by rejection. Research on
   designing for discouraged users is explicit that heavy encouragement
   backfires ("an extreme focus on maintaining engagement can make people
   anxious") and that what actually helps is refuge: warmth, low strain,
   calm. So this is ambient, not celebratory. Nothing congratulates the
   user, nothing demands attention.

   Two soft washes borrowed from landing's hero blobs (same gradient
   colors), fixed behind everything and heavily toned down. They read only
   through the page's empty margins -- cards, tables and the sidebar all
   paint opaque --surface over the top, so no text ever sits on a gradient
   and the contrast work from cycles 1-2 is untouched.

   Deliberately static. Landing's blobs drift because it's a hero you look
   at for a few seconds; this is a workspace someone sits in for an hour,
   and peripheral motion there is a distraction, not delight. */
body::before,
body::after {
  content: '';
  position: fixed;
  border-radius: 50%;
  pointer-events: none;
  z-index: -1;
  filter: blur(8px);
}
body::before {
  width: 620px; height: 620px;
  top: -220px; right: -160px;
  opacity: var(--wash-warm-op);
  background: radial-gradient(circle, #f72585 0%, #7b2ff7 45%, transparent 72%);
}
body::after {
  width: 520px; height: 520px;
  bottom: -200px; left: 120px;
  opacity: var(--wash-teal-op);
  background: radial-gradient(circle, #00f5d4 0%, transparent 70%);
}
/* Below the sidebar breakpoint the content fills the viewport edge to edge,
   so there's no empty margin left for a wash to live in -- it would just
   sit behind cards doing nothing but costing paint. */
@media (max-width: 760px) {
  body::before, body::after { display: none; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; }
/* Owl mark + bold sans "CVOWL", matching the marketing landing page's
   wordmark exactly (landing/index.html), so the logo reads as the same
   product before and after logging in. Clash Display is self-hosted from
   /static/fonts/ -- same-origin, which this app's CSP requires. */
@font-face {
  font-family: 'Clash Display';
  src: url('/static/fonts/ClashDisplay-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: 'Clash Display', var(--sans);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}
.wordmark .logo-mark { flex: none; color: var(--logo-teal); }
nav a { color: var(--ink-soft); text-decoration: none; font-size: 0.85rem; margin-left: 1rem; }

h1 { font-family: var(--serif); font-weight: 600; letter-spacing: -0.015em; font-size: 1.7rem; margin: 0 0 0.3rem; }
.lede { color: var(--ink-soft); margin: 0 0 1.8rem; font-size: 0.95rem; }

section.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow-card);
  padding: 1.6rem;
  margin-bottom: 1.6rem;
}

/* Shared table-in-a-card wrapper. A data table sitting bare on the page has
   no elevation and grows the page without bound; wrapping it gives the same
   card chrome as section.card plus its own scroll region so a long table
   scrolls internally instead of stretching the whole page. thead stays
   pinned while the body scrolls. */
.table-card {
  /* Scroll-shadow trick, both axes: four solid fade layers
     (background-attachment: local, so they scroll with the content and
     vanish at each true end) plus four radial "cast shadow" layers
     (background-attachment: scroll, fixed to the card) that only show
     while there's more to scroll in that direction -- the only cue this
     card has that it's scrollable at all, since max-height hides rows
     without it. Needs both axes, not just vertical: on desktop this table
     is wide enough that only rows overflow, but at mobile widths the
     columns themselves don't fit either (confirmed via a mobile UI sweep --
     values were clipped mid-digit with no horizontal cue at all). */
  background:
    linear-gradient(var(--surface) 30%, rgba(0, 0, 0, 0)) center top,
    linear-gradient(rgba(0, 0, 0, 0), var(--surface) 70%) center bottom,
    linear-gradient(90deg, var(--surface) 30%, rgba(0, 0, 0, 0)) left center,
    linear-gradient(90deg, rgba(0, 0, 0, 0), var(--surface) 70%) right center,
    radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) center top,
    radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) center bottom,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) right center,
    var(--surface);
  background-repeat: no-repeat;
  background-size: 100% 40px, 100% 40px, 24px 100%, 24px 100%, 100% 14px, 100% 14px, 10px 100%, 10px 100%, 100% 100%;
  background-attachment: local, local, local, local, scroll, scroll, scroll, scroll, scroll;
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow-card);
  overflow: auto;
  max-height: 62vh;
  /* Horizontal padding only -- a top pad would leave a gap above the sticky
     header where scrolled rows peek through. */
  padding: 0 1.2rem 0.4rem;
}
.table-card > table { margin: 0; }
.table-card thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
}

/* Horizontal-only variant of the scroll-shadow trick above, for a table
   sitting in a narrower container (e.g. the admin drawer panel) where the
   table's natural width exceeds the panel -- without this, `table { width:
   100% }` still lets the table overflow to its min-content width, pushing
   trailing columns (dates, in this drawer's case) off the edge of a fixed-
   position panel and out of view entirely, not just scrolled. */
.table-scroll {
  overflow-x: auto;
  background:
    linear-gradient(90deg, var(--surface) 30%, rgba(0, 0, 0, 0)) left center,
    linear-gradient(90deg, rgba(0, 0, 0, 0), var(--surface) 70%) right center,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0)) right center,
    var(--surface);
  background-repeat: no-repeat;
  background-size: 24px 100%, 24px 100%, 10px 100%, 10px 100%, 100% 100%;
  background-attachment: local, local, scroll, scroll, scroll;
}
.table-scroll > table { width: max-content; min-width: 100%; margin: 0; }

label {
  display: block;
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: 0.35rem;
}

.hidden { display: none !important; }

/* Dark-mode form fields. Every page styles its own inputs with
   `background: var(--paper)`, which in dark mode (#08080b) is DARKER than
   the --surface (#101018) card they sit inside, with a --line (#23232e)
   border that barely registers against either -- fields read as holes in
   the card rather than as things you can click and type in.

   auth.html discovered and fixed this in its own <style> block, but the
   fix never propagated, so generate.html and profile.html (the two
   heaviest forms in the product) still had invisible fields in dark mode.
   Hoisted here so it applies everywhere and can't drift again -- the
   `:root[data-theme=...]` prefix gives it higher specificity than the
   pages' own bare `input {}` rules, so it wins regardless of load order. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) input,
  :root:not([data-theme="light"]) textarea,
  :root:not([data-theme="light"]) select {
    background: var(--surface-tertiary);
    border-color: rgba(255, 255, 255, 0.3);
  }
}
:root[data-theme="dark"] input,
:root[data-theme="dark"] textarea,
:root[data-theme="dark"] select {
  background: var(--surface-tertiary);
  border-color: rgba(255, 255, 255, 0.3);
}

/* Shared button vocabulary. Two buttons side by side should read as one
   dominant + one subtle, never two equal weights -- .btn-primary is filled
   with the single brand accent, .btn-secondary is outline-only. Matches the
   reference artifact's .btn-primary/.btn-secondary exactly. */
.btn-primary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--accent);
  color: var(--paper);
  border: none;
  border-radius: 8px;
  font-family: var(--sans);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.55rem 1rem;
  min-height: 44px;
  cursor: pointer;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary svg { flex: none; }

.btn-secondary {
  background: transparent;
  border: 1.5px solid var(--line);
  border-radius: 8px;
  font-family: var(--sans);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--accent);
  padding: 0.5rem 1.1rem;
  min-height: 44px;
  cursor: pointer;
}
.btn-secondary:hover { border-color: var(--ink); background: var(--surface-tertiary); }
.btn-secondary:disabled, .btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }

/* Floating theme toggle for pages with no sidebar to embed the in-nav
   version in (auth.html, terms.html, privacy.html, ...) -- was previously
   copy-pasted per page (and simply missing on the legal pages, which
   respected a stored theme on load but gave no way to change it there). */
.theme-toggle-standalone {
  position: fixed; top: 1.2rem; right: 1.2rem; z-index: 40;
  display: flex; align-items: center; gap: 0.4rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  padding: 0.4rem 0.7rem; font-family: var(--sans); font-size: 0.78rem;
  color: var(--ink-soft); cursor: pointer;
}
.theme-toggle-standalone:hover { border-color: var(--ink); background: var(--surface-tertiary); color: var(--ink); }

/* Sidebar + content shell. The sidebar is NOT a separate fixed brand color
   in either theme -- it uses the same --surface token as cards, just a
   subtle shade off --paper. This matches how Apple's own web apps do it
   (checked live against music.apple.com: sidebar bg is a translucent,
   barely-lighter panel over the body color in both light and dark mode --
   never a persistent dark band that stays dark regardless of theme). A
   sidebar that's independently dark in light mode and then STILL
   independently dark in dark mode stops meaning anything once the content
   panel goes dark too -- there's no contrast left for it to provide. */
.app-shell { display: flex; min-height: 100vh; align-items: flex-start; }
/* Sidebar is pinned to the viewport (sticky, 100vh) rather than stretched to
   the content height. Without this, a tall page (long tracker table, long
   profile form) makes the sidebar grow with the content, pushing the footer
   (theme toggle + log out) far below the fold -- the nav count never changes
   page to page, so the footer should never move. Sticky keeps it in view and
   the footer's margin-top:auto pins it to the bottom of the 100vh column. */
.sidebar {
  width: 220px; flex: none; background: var(--surface); color: var(--ink);
  display: flex; flex-direction: column; padding: 1.6rem 1rem;
  border-right: 1px solid var(--line);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.sidebar .wordmark { font-size: 1.1rem; padding: 0 0.6rem; margin-bottom: 2rem; }
.sidebar-nav { display: flex; flex-direction: column; gap: 0.15rem; }
.sidebar-link {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.6rem 0.7rem; border-radius: 8px; margin: 0; width: 100%;
  color: rgba(var(--ink-rgb), 0.65); text-decoration: none;
  font: inherit; font-size: 0.87rem; font-weight: 500; text-align: left;
  box-sizing: border-box;
}
.sidebar-link svg { flex: none; }
.sidebar-link:hover { background: rgba(var(--ink-rgb), 0.06); color: var(--ink); }
.sidebar-link.active { background: var(--info-soft); color: var(--accent); }

.sidebar-group-toggle {
  width: 100%; background: transparent; border: none; cursor: pointer;
  font-family: var(--sans);
}
.sidebar-group-toggle .chevron { margin-left: auto; flex: none; transition: transform 0.15s ease; }
.sidebar-group-toggle[aria-expanded="true"] .chevron { transform: rotate(180deg); }
.sidebar-subnav { display: flex; flex-direction: column; gap: 0.1rem; padding-left: 2.15rem; margin-top: 0.1rem; }
.sidebar-subnav.hidden { display: none; }
.sidebar-sublink {
  display: block; padding: 0.45rem 0.7rem; border-radius: 6px;
  color: rgba(var(--ink-rgb), 0.55); text-decoration: none; font-size: 0.82rem;
}
.sidebar-sublink:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.06); }
.sidebar-sublink.active { color: var(--accent); background: var(--info-soft); }

/* Perched owl in the sidebar's dead space (see sidebar.html). margin-top:
   auto pushes it to the bottom of the nav column, just above the footer,
   so it sits in the gap that grows on short pages instead of adding new
   chrome. Faint by design -- it should register as texture you notice on
   the second or third visit, not a graphic competing with the nav. Hidden
   when the column is short enough that it would crowd the links. */
.sidebar-owl {
  margin-top: auto;
  padding: 1.4rem 0 0.6rem;
  display: flex;
  justify-content: center;
  flex: none;
}
.sidebar-owl svg {
  width: 62px;
  height: auto;
  opacity: 0.16;
}
.sidebar-owl svg path, .sidebar-owl svg circle { stroke: var(--logo-teal); }
@media (max-height: 720px) {
  .sidebar-owl { display: none; }
}

.sidebar-footer { padding-top: 1rem; border-top: 1px solid var(--line); }
.sidebar-footer .sidebar-link { color: rgba(var(--ink-rgb), 0.55); }
.sidebar-footer .sidebar-link:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.06); }
.theme-toggle { width: 100%; border: none; background: transparent; cursor: pointer; text-align: left; font-family: var(--sans); }

/* Below 760px the sidebar becomes the off-canvas drawer defined further down
   (position: fixed, slide-in). This block used to also turn it into a
   horizontally-scrolling top bar below 640px -- that predates the drawer and
   the two conflicted (both applied under 640px, so the drawer ended up
   fixed-position but still laid out as a horizontal row, clipping the
   theme toggle/log out off the right edge of the screen). Only the
   content-padding tweak survives; the sidebar itself now just uses the
   normal vertical column layout inside the drawer. */
@media (max-width: 640px) {
  .main-content { padding: 1.5rem 1rem 3rem; }
}

.main-content { flex: 1; min-width: 0; padding: 2.5rem 2rem 4rem; }

/* Mobile nav toggle + backdrop -- injected by nav.js (not hand-added to
   every page's markup, there are 5 copies of the sidebar already). Hidden
   entirely above the breakpoint where the sidebar switches to an overlay
   drawer below. */
.mobile-nav-toggle {
  display: none; position: fixed; top: 0.9rem; left: 1rem; z-index: 60;
  width: 38px; height: 38px; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink); cursor: pointer; box-shadow: var(--shadow-card);
}
.sidebar-backdrop {
  display: none; position: fixed; inset: 0; background: var(--overlay-medium); z-index: 55;
}
.sidebar-backdrop.visible { display: block; }

@media (max-width: 760px) {
  /* Sidebar was a static 220px column that never left the layout on narrow
     screens -- under ~375px that left well under 200px for content, text
     wrapping 4-5 words a line. Below this breakpoint it becomes an
     off-canvas overlay drawer instead, opened by the hamburger toggle. */
  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 56;
    transform: translateX(-100%); transition: transform 0.2s ease;
    box-shadow: var(--shadow-modal);
  }
  .sidebar.open { transform: translateX(0); }
  .mobile-nav-toggle { display: flex; }
  body.nav-open .mobile-nav-toggle { display: none; }
  /* The backdrop is `position: fixed; inset: 0`, one viewport tall -- it
     doesn't grow with the page. Without a scroll lock, scrolling the body
     behind an open drawer moves page content past the backdrop's fixed
     extent, so content further down becomes visible undimmed next to the
     drawer (found via a mobile UI sweep). Lock the body itself instead of
     trying to size the backdrop to full document height. */
  body.nav-open { overflow: hidden; }
  .main-content { padding-top: 4.2rem; }
}
.content-inner { margin: 0 auto; }

.page-header-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; margin-bottom: 2rem; }

.status-line {
  font-family: var(--sans);
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.msg {
  font-size: 0.85rem;
  padding: 0.8rem 1rem;
  margin-bottom: 1.1rem;
  border-radius: 8px;
  display: none;
}
.msg.error { display: block; background: var(--danger-soft); border: 1px solid var(--danger); color: var(--ink); }
.msg.success { display: block; background: var(--safe-soft); border: 1px solid var(--safe); color: var(--ink); }
.msg.info { display: block; background: var(--paper); border: 1px solid var(--line); color: var(--ink-soft); }

/* Sitewide interactive transitions -- smooths hover/state changes that
   previously snapped instantly (no transition property at all). Motion
   only, no new shapes/colors/shadows -- see DESIGN.md and the UI-delight
   /plan-ceo-review (2026-07-22). */
button.primary,
button.ghost,
.files-btn,
.jd-tab,
.template-option,
.split-btn a,
.fact-check-confirm-btn,
.files-popover a {
  transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease, transform 0.15s ease, color 0.15s ease;
}

/* Loading state -- three static dots with a staggered opacity cycle,
   replacing bare "Loading…" text. Signals "working," not "broken," no
   spinner icon or brand mark. See the UI-delight /plan-ceo-review
   (2026-07-22). */
.loading-dots { color: var(--ink-soft); letter-spacing: 0.15em; }
.loading-dots span {
  display: inline-block;
  animation: loading-dot-pulse 1.2s ease-in-out infinite;
}
.loading-dots span:nth-child(2) { animation-delay: 0.25s; }
.loading-dots span:nth-child(3) { animation-delay: 0.5s; }
@keyframes loading-dot-pulse {
  0%, 80%, 100% { opacity: 0.25; }
  40% { opacity: 1; }
}

/* Info icon + tooltip -- pure CSS, no JS. Sits next to a label/heading,
   content comes from data-tip. Hover or keyboard focus reveals it. */
.info-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  margin-left: 0.35em;
  border-radius: 50%;
  border: 1px solid var(--ink-soft);
  color: var(--ink-soft);
  font-size: 0.68rem;
  font-weight: 600;
  font-style: normal;
  line-height: 1;
  cursor: help;
  position: relative;
  vertical-align: middle;
  user-select: none;
}
.info-icon::before { content: "i"; }
.info-icon:hover, .info-icon:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
  outline: none;
}
.info-icon::after {
  content: attr(data-tip);
  position: absolute;
  z-index: 20;
  bottom: 130%;
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 260px;
  padding: 0.5em 0.7em;
  border-radius: 6px;
  background: var(--ink);
  color: var(--paper);
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.info-icon:hover::after, .info-icon:focus-visible::after {
  opacity: 1;
}

/* Blanket reduced-motion override -- written once here, not per-selector,
   so no new animated rule can be added later without already respecting
   it. Resolved as a structural constraint (not a final checklist item)
   during the UI-delight /plan-ceo-review (2026-07-22). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
