/*
 * Brand Tokens — the single authoritative source of brand truth.
 *
 * Loaded by EVERY page. These are the canonical color, typography, spacing,
 * radius, elevation, and motion values in light and dark themes. Pages and
 * reusable components reference these tokens instead of hard-coded literals so
 * the brand cannot drift between pages.
 *
 * ─────────────────────────────────────────────────────────────────────────
 * BRAND DIRECTION (2026 refresh) — "Studio"
 * ─────────────────────────────────────────────────────────────────────────
 * Positioning: a calm, editorial authoring workspace — content is the hero,
 * chrome recedes (learning from Articulate Rise & Linear). We deliberately
 * avoid the generic "AI aesthetic" (rainbow gradients, glow orbs, everything
 * animated) that now reads as cheap.
 *
 * Signature: Indigo remains the confident primary, but our OWNABLE, distinct
 * mark is the warm "Spark" coral — the creative moment of authoring — which
 * sets us apart from the all-purple AI crowd.
 *
 * Gradient discipline: saturated brand gradient ONLY on the primary action and
 * the signature "generate" moment; subtle washes in backgrounds; calm, flat
 * content cards. Never a three-stop rainbow.
 * ─────────────────────────────────────────────────────────────────────────
 */
:root {
  /* ---- Core brand palette ---- */
  --brand-indigo: #4f46e5;   /* PRIMARY — confident, trustworthy */
  --brand-violet: #7c3aed;   /* secondary — pairs with indigo */
  --brand-spark:  #f2683c;   /* SIGNATURE warm coral — the creative "spark" */
  --brand-spark-strong: #db4f22; /* text-safe coral for on-light labels */
  --brand-cyan:   #0891b2;   /* informational only */
  --brand-green:  #059669;   /* success */
  --brand-red:    #dc2626;   /* danger */
  --brand-amber:  #d97706;   /* warning */
  /* Soft lilac highlight wash. Taken from the ElevenLabs studio, where selected
   * narration text sits on #F5D2FE: a desaturated tint that marks a range
   * without shouting, and stays legible under near-black ink (16:1 with
   * --text-primary). Used for ::selection and passive "this is the bit I mean"
   * highlights — never as a fill for an action, which is the accents' job. */
  --brand-lilac:  #f5d2fe;
  /* Ink for anything sitting on --brand-lilac. Theme-invariant BY DESIGN: the
   * lilac is a light wash in dark theme too, so this must not follow
   * --text-primary or selected text turns near-white on near-white. */
  --selection-fg: #0f0f10;

  /* ---- Canonical accents (existing page names, pinned here as truth) ---- */
  --accent-violet: var(--brand-violet);
  --accent-indigo: var(--brand-indigo);
  --accent-spark:  var(--brand-spark);
  --accent-cyan:   var(--brand-cyan);
  --accent-green:  var(--brand-green);
  --accent-red:    var(--brand-red);
  --accent-amber:  var(--brand-amber);
  --accent-lilac:  var(--brand-lilac);

  /* ---- Gradients (disciplined) ----
   * gradient-main: the primary two-stop brand gradient (indigo→violet). Used
   *   on the primary CTA and the nav mark. Tight, ownable, NOT a rainbow.
   * gradient-spark: the signature "AI generate" gradient (indigo→spark coral).
   *   Reserve this for the single most important creative action on a screen.
   * gradient-wash: barely-there background tint for large calm surfaces. */
  --gradient-main:  linear-gradient(135deg, var(--brand-indigo) 0%, var(--brand-violet) 100%);
  --gradient-spark: linear-gradient(120deg, var(--brand-indigo) 0%, var(--brand-spark) 100%);
  --gradient-wash:  linear-gradient(135deg, rgba(79,70,229,0.06) 0%, rgba(242,104,60,0.04) 100%);

  /* ---- Status gradients ----
   * Pages kept hand-writing `linear-gradient(135deg,#0891b2,#06b6d4)` for an
   * info button, `#059669,#10b981` for success, and so on. Those literals
   * survive a palette change and quietly contradict it, so the status fills
   * live here, derived from the status accents above. */
  --gradient-info:    linear-gradient(135deg, var(--accent-cyan) 0%, color-mix(in srgb, var(--accent-cyan) 70%, #ffffff) 100%);
  --gradient-success: linear-gradient(135deg, var(--accent-green) 0%, color-mix(in srgb, var(--accent-green) 70%, #ffffff) 100%);
  --gradient-warn:    linear-gradient(135deg, var(--accent-amber) 0%, color-mix(in srgb, var(--accent-amber) 74%, #ffffff) 100%);
  --gradient-danger:  linear-gradient(135deg, var(--accent-red) 0%, color-mix(in srgb, var(--accent-red) 74%, #ffffff) 100%);

  /* On-color foregrounds (WCAG AA against their backgrounds) */
  --on-brand: #ffffff;       /* text/icons on indigo/violet/spark fills */
  /* Brand ink: the deep navy base of the dark chrome (it is the first stop of
   * --sidebar-bg). Pinned as its own token so anything that needs to deepen an
   * accent — e.g. the Home page's animated template covers — mixes toward the
   * SAME dark as the sidebar instead of inventing a near-black. */
  --brand-ink: #0b1228;

  /* ---- Typography ---- */
  /* INTER TIGHT, not Inter.
   *
   * "the site fonts are not good it should be different font that take less space
   * and make feel our site modern progressive app thin app ... now the app looks
   * heavy boring old style."
   *
   * Inter is the safe 2018 product-UI default, and that is exactly the problem:
   * it is wide. Its generous side bearings are what pushed a three-column layout
   * into truncating everything it holds - "Refactoring a messy codebas…" in the
   * module tree, "Pluralsight c…" in the review rail. Every one of those ellipses
   * was a width problem being solved by deleting the author's words.
   *
   * Inter Tight is the same typeface family by the same designer with the
   * horizontal metrics pulled in (~4-6% narrower at the same optical size). So:
   *   - the SAME size tokens now fit more characters, which un-truncates the rails
   *     without shrinking any text or reflowing any layout;
   *   - the vertical metrics, x-height and weight axis are unchanged, so nothing
   *     that depends on line-height or row height moves;
   *   - it reads tighter and more current, which is the "thin modern app" ask.
   *
   * Inter stays in the stack directly behind it: it is already cached from the
   * previous build and is the closest possible fallback if Inter Tight fails to
   * load, so a cold cache degrades to near-identical type rather than to Arial.
   *
   * Licence: SIL OFL 1.1, same as Inter. Open source, no per-seat cost, and
   * self-hostable if the CDN ever has to go. */
  --font-sans: 'Inter Tight', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', Menlo, Consolas, monospace;
  /* Editorial serif, used only for long-form reading surfaces (the design
   * rationale document). rationale.html declared this stack in its own :root,
   * which meant a third typeface existed outside the brand truth; it is pinned
   * here so the brand owns every face the app can render. */
  --font-serif: 'Newsreader', Georgia, 'Times New Roman', serif;

  /* Type scale (fluid-friendly rem steps) */
  /* Type scale — 9 steps, and nothing between them.
   *
   * The scale existed before this but almost nothing referenced it: an audit
   * found 104 DISTINCT font-size values hardcoded across the CSS and HTML
   * (0.7, 0.72, 0.74, 0.75, 0.76, 0.78, 0.8, 0.82, 0.85 ...). That is why the
   * type looked heavy and inconsistent rather than any single size being wrong —
   * nine near-identical sizes in one view read as sloppiness even when each one
   * is defensible.
   *
   * Sizes are px-derived on a 16px root so they land on whole pixels, and the
   * ramp is tuned to the dense-product convention: 14px body, 13px UI text,
   * 12px labels. `--text-base` moved 0.95rem -> 0.875rem, which is the single
   * change that makes the app read as lighter.
   *
   * Rule: use a token. If a new size seems necessary, the design needs a
   * different step, not a new value.
   */
  /* ---- The lower steps are FIXED, and that is the mobile decision ----
   *
   * These four are already at the readability floor for a dense product, so a
   * phone must not shrink them. responsive.css used to step `body` down to
   * --text-sm and table cells to --text-xs at 480px, which made the smallest
   * text on the smallest screen smaller still: 11px meta on a 390px phone held
   * at arm's length. That is backwards, and it is why the app read as cramped
   * rather than as any single size being wrong. Those step-downs are gone.
   */
  --text-2xs:  0.6875rem;  /* 11px - dense meta, badge text            */
  --text-xs:   0.75rem;    /* 12px - labels, table headers, captions   */
  --text-sm:   0.8125rem;  /* 13px - nav rows, buttons, secondary body */
  --text-base: 0.875rem;   /* 14px - body copy                         */
  /* ---- The upper steps are FLUID, with the desktop value as the CEILING ----
   *
   * A 32px page title is 8% of a 390px viewport before any padding, so the big
   * steps have to respond. Written as clamp() rather than as media-query
   * step-downs for two reasons: the size then adapts at every width instead of
   * jumping at five arbitrary ones, and it removes the h1/h2/h3 overrides that
   * responsive.css was carrying at both 768 and 480 - four rules replaced by
   * the token knowing its own range.
   *
   * The max is exactly the previous fixed value, so DESKTOP IS UNCHANGED BY
   * CONSTRUCTION. That is what makes this safe to do to a 23-page app in one
   * edit: the only viewports that can move are the narrow ones.
   *
   * Floors are one step down the scale, so the ramp stays recognisable on a
   * phone rather than collapsing to a single size.
   */
  --text-md:   1rem;                                    /* 16px - emphasised body, card titles */
  --text-lg:   clamp(1rem, 0.95rem + 0.6vw, 1.125rem);  /* 16 -> 18px - section headings */
  --text-xl:   clamp(1.125rem, 1rem + 1.1vw, 1.375rem); /* 18 -> 22px - dialog titles */
  --text-2xl:  clamp(1.375rem, 1.1rem + 2.2vw, 2rem);   /* 22 -> 32px - the page title */
  /* Landing hero only. Fluid on purpose: it is the one place a size may respond
     to the viewport, because it is the only full-bleed heading in the app. */
  --text-3xl:  clamp(2rem, 4vw, 3rem);
  --tracking-tight: -0.02em;   /* headings */
  --tracking-body:  -0.011em;  /* body */

  /* ---- Type weight scale ----
   * The app reached for 800 in 117 places and 900 in 16. When almost everything
   * is extra-bold nothing reads as emphasis, the hierarchy flattens, and the
   * result looks like a consumer app rather than a professional tool. Weight is
   * the loudest typographic signal there is, so it is rationed here:
   *
   *   display  page titles and the one or two things that must land first
   *   strong   card titles, section headings, the primary button
   *   bold     labels, table headers, small caps
   *   medium   body copy that needs a touch more presence than regular
   *
   * Named rather than numeric so a future adjustment is one edit, not 300. */
  --weight-display: 700;
  --weight-strong: 650;
  --weight-bold: 600;
  --weight-medium: 500;

  /* ---- Icon size scale ----
   * There was no icon token at all, and the consequence was that eight
   * independent places set an icon's dimensions: the rail at 20px docked and
   * 22px in its drawer, the topbar at 15px, its credit dot at 13px, the toast
   * at 20px, plus rules in editor-shared.js and three in home.html. So "the
   * icons are too big on mobile" had no lever to pull - it had eight, and
   * nobody could know if they had found them all.
   *
   * Values are the sizes already in use, so naming them changes nothing on
   * screen. That is deliberate: this commit makes icon size ADJUSTABLE, and a
   * token that also changes what you see cannot be reviewed for either.
   *
   * --icon-md is the default. Reach for a different step only when the icon is
   * in genuinely denser or larger chrome, the same rule the type scale follows.
   */
  --icon-xs: 13px;   /* credit dot, inline meta glyphs */
  --icon-sm: 15px;   /* topbar controls, dense action rows */
  --icon-md: 20px;   /* the default: rail tiles, toasts, most buttons */
  --icon-lg: 22px;   /* touch drawer rows, where the target is thumb-sized */

  /* ---- Minimum tap target ----
   * The smallest a control may be when a FINGER is what is pointing at it. An
   * icon size and a target size are different things: the glyph stays on the
   * --icon-* scale above while the hit area grows to this, so a 15px icon can
   * still own a 44px target.
   *
   * 44px because that is what iOS asks for (Material asks 48dp); it is the
   * larger of the two floors that every platform guideline agrees on, and it
   * satisfies WCAG 2.5.5. Applied only under `@media (pointer: coarse)`, since
   * what makes a small control hard to hit is the input device rather than the
   * screen: a 1024px tablet needs it and a desktop window dragged narrow does
   * not. Consumed with a `44px` fallback so it still holds on the pages that do
   * not load this file.
   */
  --tap-min: 44px;

  /* ---- Spacing scale ---- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-8: 3rem;

  /* ---- Radius ---- */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-full: 999px;

  /* ---- Elevation (soft, calm — no heavy glow in light mode) ---- */
  --shadow-card: 0 1px 3px rgba(16,24,40,0.06), 0 1px 2px rgba(16,24,40,0.04);
  --shadow-card-hover: 0 10px 30px rgba(16,24,40,0.10);
  --shadow-popup: 0 20px 60px rgba(16,24,40,0.22);
  /* Branded lift: the ONE elevation that carries the brand colour, for the
   * single emphasised surface on a screen (the "most popular" pricing card, a
   * featured tile). The neutral --shadow-card* above must stay neutral — a card
   * grid where every card glows is noise — so this is deliberately separate and
   * used sparingly. It was hand-written as `0 8px 24px color-mix(... 15%, ...)`
   * on landing.html's .price.featured; derived from --brand-indigo here so a
   * tenant's palette flows through instead of a literal indigo surviving it. */
  --shadow-brand: 0 8px 24px color-mix(in srgb, var(--brand-indigo) 15%, transparent);

  /* ---- Scrim (the dim behind a popup) ----
   * Every overlay wrote its own: the shared modal dimmed to rgba(10,15,26,.55),
   * the full-screen reader to rgba(15,20,35,.55) and the side drawer to
   * rgba(15,20,35,.35) — three different darks behind three popups in one app.
   * One token for a blocking overlay, one for a lighter drawer scrim. */
  --scrim: rgba(10,15,26,0.55);
  --scrim-soft: rgba(10,15,26,0.35);

  /* ---- Motion (purposeful, not decorative) ---- */
  --motion-fast: 0.15s ease;
  --motion-base: 0.25s ease;
  --motion-slow: 0.4s ease;

  /* ---- Interaction response ----
   * A branded FILL (gradient button, nav CTA) responds to hover by brightening
   * and to press by dimming. It must never repaint its own background on hover:
   * a hover rule that changes a background without also owning the foreground is
   * exactly how the landing page's "Launch app" label dissolved — a 6% indigo
   * tint painted under text that a `!important` kept white.
   *
   * The steps were literals, and they had drifted: brightness(1.06) in five
   * files, 1.07 in the sidebar's New course button, 1.08 in three more — the
   * same gesture with three different amounts depending on which page you were
   * on. --tint-hover is the flat-surface counterpart: the faint wash under a
   * text link or row on hover, which several pages hand-wrote as
   * color-mix(... 6%, transparent).
   *
   * Rule: brightness(var(--hover-brighten)) on a branded fill,
   * var(--tint-hover) on a plain surface. Nothing else. */
  /* ---- Bottom-right corner stack ----
   * THREE separate floating things claimed this corner independently — the
   * "Ask AI" FAB (fixed, 1.5rem/1.5rem), the document jump buttons (absolute,
   * 1.1rem/1.1rem) and the toast (fixed, 1.5rem/1.5rem) — so they sat on top of
   * one another: the purple FAB covered the up/down arrows, and a toast landed
   * across both.
   *
   * Nothing was wrong with any single rule; the corner simply had no owner. These
   * tokens make it one coordinate system, so a new floating element takes a LANE
   * instead of guessing an inset and colliding.
   *
   *   --fab-inset   distance from the viewport corner
   *   --fab-size    the FAB's diameter (it owns the corner itself)
   *   --fab-gap     breathing room between stacked items
   *   --corner-lane-2   bottom offset for the row ABOVE the FAB (jump buttons)
   *   --corner-clear-x  right offset that clears the FAB column (toasts)
   */
  --fab-inset: 1.5rem;
  --fab-size: 56px;
  --fab-gap: 0.75rem;
  --corner-lane-2: calc(var(--fab-inset) + var(--fab-size) + var(--fab-gap));
  --corner-clear-x: calc(var(--fab-inset) + var(--fab-size) + var(--fab-gap));
  /* Height of the jump-button pair that occupies lane 2 (two 2.1rem circles plus
   * their 0.3rem gap), and the lane ABOVE it. Needed on narrow screens, where a
   * toast cannot sit beside the column and has to clear the whole thing instead —
   * measured at 390px, a mobile toast placed in lane 2 landed straight on the
   * jump buttons. */
  --corner-jump-h: 4.5rem;
  --corner-lane-3: calc(var(--corner-lane-2) + var(--corner-jump-h) + var(--fab-gap));

  --hover-brighten: 1.06;
  --active-dim: 0.97;
  --tint-hover: color-mix(in srgb, var(--brand-indigo) 6%, transparent);

  /* ---- Secondary / outline control hover ----
   * A SECONDARY control (.btn-secondary, .btn.btn-ghost, and the marketing
   * pages' .btn-ghost / .btn-ghost-d / pricing .btn-outline) answers hover by
   * lifting its SURFACE and warming its border, and it keeps its label at
   * --text-primary. Two things it must not do:
   *
   *   - Fill with the brand colour. A saturated brand fill is the PRIMARY
   *     action's signal (see "Gradient discipline" above), so a secondary that
   *     paints itself solid indigo on hover reads as a second primary next to
   *     the real one.
   *   - Recolour the LABEL to --brand-indigo. Indigo measures 2.9:1 against the
   *     dark page surface, under the 3:1 floor for a control's label — which is
   *     the reason the gesture lives on the surface rather than the text.
   *
   * Pinned here because it had been hand-written three times: ui-kit.css mixed
   * against its own --uk-hairline, landing.html's hero secondary used a 4%
   * indigo tint, and landing's pricing secondary filled solid indigo. Same
   * button role, same resting declarations, three different hovers.
   *
   * Both values derive from theme tokens, so dark theme follows automatically
   * (custom properties substitute at use time, against the element's theme). */
  --btn-secondary-hover-bg: var(--bg-card-hover);
  --btn-secondary-hover-border: color-mix(in srgb, var(--accent-violet) 45%, var(--border-subtle));

  /* ---- Focus ring (accessibility — visible, brand-colored) ---- */
  --focus-ring: 0 0 0 3px rgba(79,70,229,0.35);

  /* ---- Light theme surfaces ----
   * NEUTRAL greys, not blue-grey. This is the single change that separates our
   * chrome from the ElevenLabs reference we have been aiming at since the type
   * was picked ("Inter: clean, high-contrast type like the ElevenLabs UI").
   *
   * Every neutral here used to carry a blue cast: #f7f8fb page, #0b0d12 ink,
   * #3a4150 / #5b6472 secondary text, #e5e7eb hairlines. Individually each is
   * defensible; together a whole app of blue-tinted greys is what reads as a
   * 2018 enterprise dashboard. Inspecting ElevenLabs shows the opposite choice:
   * ink at #0F0F10, which is very nearly pure neutral, on a warm off-white.
   *
   * Warm-neutral instead. Nothing about the layout, type or spacing moves - only
   * the hue of the greys - and the surfaces read calmer and more current because
   * the page stops competing with the indigo/violet accents sitting on top of it.
   * Contrast is preserved or improved: ink 19:1, secondary 10:1, muted 5.2:1 on
   * --bg-primary, so every pair still clears WCAG AA. */
  --bg-primary: #f6f5f3;      /* warm off-white, the ElevenLabs page surface */
  --bg-secondary: #ffffff;
  --bg-card: #ffffff;
  --nav-bg: rgba(255,255,255,0.82);
  --text-primary: #0f0f10;    /* ElevenLabs ink — neutral near-black */
  --text-secondary: #3f4043;
  --text-muted: #6e6e73;
  --border-subtle: #e7e6e2;   /* warm hairline */

  /* Surfaces used by individual pages — pinned here so pages can inherit
   * instead of redefining their own :root (prevents brand drift). */
  --bg-card-hover: #faf9f7;
  --input-bg: #faf9f7;
  --guidelines-bg: #f2f1ed;
  --preview-bg: #faf9f7;
  --border-glow: rgba(79,70,229,0.2);
  --radius-xl: 24px;
  --gradient-subtle: linear-gradient(135deg, rgba(79,70,229,0.06), rgba(124,58,237,0.03), rgba(242,104,60,0.02));
  /* Table / meter surfaces (were re-declared per page as --track-bg, --row-hover). */
  --track-bg: #eae9e5;
  --row-hover: color-mix(in srgb, var(--accent-violet) 4%, transparent);

  /* Legacy surface aliases. Older pages (workspace.html) style borders, cards
   * and inputs with the short names --border / --shadow / --bg-soft. Those were
   * never defined at :root, so in LIGHT theme every `var(--border)` and
   * `var(--shadow)` was an invalid substitution and the page rendered cards
   * with no border and no elevation. Aliasing them to the canonical tokens
   * fixes that once, and keeps the short names honest if a page still uses them. */
  --border: var(--border-subtle);
  --shadow: var(--shadow-card);
  --bg-soft: var(--input-bg);

  /* ---- Generated-document typography ----
   * The rendered outline/slides/narration previews colour their headings and
   * module labels. Those were literal hexes on several pages, so a customer's
   * palette stopped at the edge of the document. Semantic tokens instead. */
  --doc-heading: var(--accent-violet);
  --doc-module: var(--brand-spark-strong);
  /* The rendered document is a PAGE — it is white in BOTH themes, the way the
   * exported .docx is, so it cannot use the theme surfaces (dark-theme ink on a
   * white page is invisible). These were literal hexes repeated across
   * home.html's preview rules; pinned here they stay theme-invariant BY DESIGN,
   * a palette change can still reach them, and their contrast against
   * --doc-paper is fixed rather than accidental. --doc-meta is deliberately NOT
   * var(--text-muted): the theme's muted grey drops to ~2.5:1 on white paper. */
  --doc-paper: #ffffff;         /* the page itself */
  --doc-paper-muted: #f3f4f6;   /* the gray mat the page floats on */
  --doc-ink: #0b0d12;           /* headings / strong — highest contrast */
  --doc-body: #3a4150;          /* body copy on the page */
  --doc-meta: #5b6472;          /* de-emphasised copy, still AA on paper */
  --doc-border: #e5e7eb;        /* rules and table borders on the page */

  /* ---- Subject-domain identity ----
   * Course covers need a set of visually DISTINCT domain colours (a course
   * gallery where every card is the same hue is unreadable), but they were
   * hard-coded in JavaScript, in two different files. They are tokens now, so
   * they are themeable and defined exactly once. Each pair is (from, to) for
   * the animated cover gradient. */
  --domain-ai-from: var(--brand-indigo);       --domain-ai-to: var(--brand-violet);
  --domain-cloud-from: #0891b2;                --domain-cloud-to: #2563eb;
  --domain-security-from: #1e293b;             --domain-security-to: #0f172a;
  --domain-code-from: #6d28d9;                 --domain-code-to: var(--brand-indigo);
  --domain-data-from: #059669;                 --domain-data-to: #0d9488;
  --domain-business-from: #334155;             --domain-business-to: var(--brand-indigo);
  --domain-finance-from: #be123c;              --domain-finance-to: #db2777;
  --domain-design-from: #d97706;               --domain-design-to: #e11d48;
  --domain-marketing-from: #0284c7;            --domain-marketing-to: var(--brand-violet);

  /* ---- Tenant brand aliases ----
   * A simple, stable vocabulary tenants (and the demo-repo branding export)
   * can target, derived from the tokens above so the customer's chosen
   * palette flows through automatically. Contrast between --brand-on-primary
   * and --brand-primary must satisfy WCAG 2.2 AA; the Appearance panel's
   * contrast gate blocks a failing pair. */
  --brand-primary: var(--accent-violet, #7c3aed);
  --brand-secondary: var(--accent-indigo, #4f46e5);
  /* The customer's third colour is the "spark" accent they pick in Appearance
   * (--accent-spark). This used to alias --accent-cyan, which is an
   * informational status colour the panel does not control — so a chosen
   * accent never reached anything styled with --brand-accent. */
  --brand-accent: var(--accent-spark, #f2683c);
  /* Contrast-safe foreground for --brand-primary. brand-runtime.js computes
   * --on-brand from the chosen primary's luminance, so dark text is used on a
   * light primary instead of unreadable white. */
  --brand-on-primary: var(--on-brand, #ffffff);
  --brand-surface: var(--bg-card, #ffffff);
  --brand-on-surface: var(--text-primary, #1f2937);
  /* Defaults to the two-stop brand gradient, never a three-stop rainbow. */
  --brand-gradient: var(--gradient, var(--gradient-main,
    linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)));

  /* Page-level foreground/background branding aliases. These give tenants a
   * simple pair to override the overall page palette; they default to the
   * product tokens so unbranded tenants render unchanged. --brand-fg on
   * --brand-bg must satisfy WCAG 2.2 AA (Req 13.6); the Settings_UI contrast
   * gate (Req 7.9/7.10) blocks publication of failing pairs. */
  --brand-bg: var(--bg-primary, #ffffff);
  --brand-fg: var(--text-primary, #1f2937);
  --brand-muted: var(--text-muted, #6b7280);
  --brand-border: var(--border, var(--border-subtle, #e5e7eb));
  --brand-focus: var(--brand-primary);         /* focus-ring color (accessibility) */

  /* Brand assets: set to url(...) by tenant branding; `none` => Default_Branding */
  --brand-logo: none;      /* logo image, shown in chrome header */
  --brand-icon: none;      /* square icon / favicon-style mark */
  --brand-banner: none;    /* optional wide banner image */

  /* ---- App sidebar (light chrome, matching the rest of the app) ----
   * The rail used to be a deliberately dark navy in BOTH themes. Next to an
   * otherwise white product that read as a leftover from another design: it
   * fought the page instead of framing it, and its icons had to be drawn in
   * white alphas that nothing else in the app used.
   *
   * It is a LIGHT surface now, and it still gets its own tokens rather than
   * borrowing the theme surfaces, because it is chrome: it sits a step back from
   * the content it frames (a hair grey rather than paper white) and the dark
   * theme overrides these below. Pinned here as the single source of truth so the
   * nav chrome cannot drift. */
  /* ---- Breakpoints ----
   * An audit found 20 distinct @media widths across the frontend - 1360, 1200,
   * 1180, 1101, 1100, 1000, 900, 820, 800, 768, 760, 720, 700, 681, 680, 640,
   * 620, 600, 560, 480 - most of them used by exactly one file. That is why one
   * page reflowed at 700 and the page beside it at 720.
   *
   * Five, and no more. Declared here so JS and CSS read the same numbers:
   * @media cannot interpolate a custom property, so these do not remove the
   * literals, they make the literals CHECKABLE. The enforcement is
   * test_the_app_is_mobile_friendly.py, which fails on any width that is not one
   * of these.
   *
   * --bp-tablet (900) and --bp-tablet-portrait (768) are LOAD-BEARING:
   * components.js reads matchMedia('(max-width: 900px)') for the sidebar mode
   * and home.html reads innerWidth < 768 for the workflow rail. Moving either
   * desynchronises the CSS from the JS.
   */
  --bp-laptop: 1100px;
  --bp-tablet: 900px;
  --bp-tablet-portrait: 768px;
  --bp-phone-lg: 640px;
  --bp-phone: 480px;

  --app-sidebar-w: 248px;
  /* Icon-rail width: the Studio layout's default left chrome. Wide enough for
   * an icon tile with its tiny label ("Narration" is the longest) underneath. */
  --app-sidebar-rail-w: 68px;
  /* Global top bar (<app-topbar>): page label, global search, notifications,
   * and the account menu. Height is pinned here so any page that needs to
   * reserve or offset space uses the same number as the component. */
  --app-topbar-h: 56px;
  --sidebar-bg: #f2f1ed;                    /* a step back from paper white, warm */
  --sidebar-border: #e7e6e2;
  --sidebar-fg: #0f0f10;                    /* logo / active label */
  --sidebar-link: #55565a;                  /* idle link text, AA on --sidebar-bg */
  --sidebar-hover-bg: #e9e8e3;
  --sidebar-active-bg: color-mix(in srgb, var(--accent-violet, #7c3aed) 12%, transparent);
  --sidebar-control-bg: #ffffff;
  --sidebar-control-border: #dcdbd6;
  --sidebar-control-hover-bg: #e9e8e3;
}

[data-theme="dark"] {
  /* The sidebar is a light surface in light theme, so dark theme has to restate
     it or the rail would be near-white against a dark page. Same role, inverted:
     one step back from the page, not a different design. */
  --sidebar-bg: #0d1322;
  --sidebar-border: rgba(148, 163, 184, 0.20);
  --sidebar-fg: #f3f4f6;
  --sidebar-link: #b6bfce;
  --sidebar-hover-bg: rgba(255, 255, 255, 0.07);
  --sidebar-active-bg: color-mix(in srgb, var(--accent-violet, #7c3aed) 26%, transparent);
  --sidebar-control-bg: rgba(255, 255, 255, 0.06);
  --sidebar-control-border: rgba(255, 255, 255, 0.20);
  --sidebar-control-hover-bg: rgba(255, 255, 255, 0.14);

  --bg-primary: #0a0f1a;
  --bg-secondary: #0f1524;
  --bg-card: rgba(18, 24, 40, 0.7);
  --nav-bg: rgba(10, 15, 26, 0.72);
  --text-primary: #f3f4f6;
  --text-secondary: #cbd2df;
  --text-muted: #9ca3af;
  --border-subtle: rgba(148, 163, 184, 0.18);
  --shadow-card: 0 1px 3px rgba(0,0,0,0.4);
  --shadow-card-hover: 0 12px 32px rgba(0,0,0,0.45);
  --shadow-popup: 0 20px 60px rgba(0,0,0,0.6);
  --focus-ring: 0 0 0 3px rgba(124,58,237,0.45);
  /* Dark mode may use a slightly brighter spark for legibility */
  --brand-spark: #ff7b4f;

  --bg-card-hover: rgba(24, 32, 56, 0.8);
  --input-bg: rgba(10, 15, 26, 0.6);
  --guidelines-bg: rgba(10, 15, 26, 0.5);
  --preview-bg: rgba(10, 15, 26, 0.5);
  --border-glow: rgba(124,58,237,0.3);
  --gradient-subtle: linear-gradient(135deg, rgba(124,58,237,0.15), rgba(79,70,229,0.08), rgba(242,104,60,0.06));
  --track-bg: rgba(148,163,184,0.22);
  --row-hover: color-mix(in srgb, var(--accent-violet) 10%, transparent);
  /* 6% of a saturated indigo is invisible on a dark page, so the same gesture
     needs more of it here to read as the same amount of feedback. */
  --tint-hover: color-mix(in srgb, var(--brand-indigo) 16%, transparent);
}
