/* =============================================================================
   Font delivery — MemeLab Console (self-hosted, ADR-001 §4)

   Unbounded (display), Onest (body), JetBrains Mono (mono) — variable woff2
   from @fontsource-variable, declared under the ORIGINAL family names so
   tokens and consumers keep using Unbounded / Onest / JetBrains Mono.

   postcss-url (postcss.config.mjs) copies each referenced woff2 into
   dist/fonts/<hash>.woff2 and rewrites the url() — no external requests,
   no fonts.googleapis.com. Regenerate blocks from
   node_modules/@fontsource-variable/<pkg>/index.css when bumping fontsource.
   ============================================================================= */

/* unbounded-cyrillic-ext-wght-normal */

@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url(../fonts/27ba3258.woff2) format('woff2-variations');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* unbounded-cyrillic-wght-normal */

@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url(../fonts/8c165af8.woff2) format('woff2-variations');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* unbounded-vietnamese-wght-normal */

@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url(../fonts/3fb880d5.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* unbounded-latin-ext-wght-normal */

@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url(../fonts/4e28931f.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* unbounded-latin-wght-normal */

@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-display: swap;
  font-weight: 200 900;
  src: url(../fonts/c16ed084.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* onest-cyrillic-ext-wght-normal */

@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(../fonts/62a16e6.woff2) format('woff2-variations');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* onest-cyrillic-wght-normal */

@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(../fonts/a9d9fbfb.woff2) format('woff2-variations');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* onest-latin-ext-wght-normal */

@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(../fonts/89b3611f.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* onest-latin-wght-normal */

@font-face {
  font-family: 'Onest';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(../fonts/75799c92.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* jetbrains-mono-cyrillic-ext-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/f7af370c.woff2) format('woff2-variations');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* jetbrains-mono-cyrillic-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/31ebbd13.woff2) format('woff2-variations');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* jetbrains-mono-greek-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/6d5edb17.woff2) format('woff2-variations');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* jetbrains-mono-vietnamese-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/1b2972d9.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* jetbrains-mono-latin-ext-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/f12836f9.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* jetbrains-mono-latin-wght-normal */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url(../fonts/735060de.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

:root {
  /* ============================================================
     MemeLab Console — token values (dark theme).
     Phase 1 of the "MemeLab Console" redesign: warm-charcoal flat
     surfaces, a single electric-violet accent, hairline borders,
     no glass/neon. Light theme ([data-theme="light"]) lands in a
     later phase. Token NAMES are kept stable so the 65 components
     keep working — only the VALUES change.
     ============================================================ */

  /* ---- Background (warm charcoal) ---- */
  --ml-bg: 13 12 11;
  --ml-fg: 246 244 241;
  /* The foreground ladder needs channel triples too, not just hex aliases:
     without them the Tailwind preset cannot offer `text-fg-2` / `text-fg-3`,
     and every consumer reaches for `text-white/40` instead — which lands at
     3.8:1 on --bg and fails AA. Keep in step with --fg-2 / --fg-3 below. */
  --ml-fg-2: 176 171 163;
  /* 151 146 136, not 145 140 130. The quiet rung was BELOW the line, not on it:
     on a --fill-2 chip (white at 9%) over --bg-raised the old tuple measures
     4.27:1, and Streamradar and the chat console hit that same ground
     independently. Four percent lighter is invisible to the eye and clears
     4.62:1 there, 6.3:1 on the page.
     (This comment said 4.49 until 0.49.0 — a number nothing in the tree could
     reproduce. A stale measurement in a comment is worse than none: it reads as
     «one hundredth short», which invites putting the old tuple back.) */
  --ml-fg-3: 151 146 136;

  /* ---- Surface ladder (raised → hover → deeper steps) ---- */
  --ml-surface-0: 13 12 11; /* canvas / bg */
  --ml-surface-50: 23 21 19; /* panels, cards, sidebar (bg-raised) */
  --ml-surface-100: 31 28 25; /* hover / slightly raised (bg-hover) */
  --ml-surface-200: 40 36 32;
  --ml-surface-300: 51 46 41;
  --ml-surface-400: 64 58 51;

  /* ---- Hairlines / fills (consumed by flattened surfaces) ---- */
  --ml-line: 255 255 255; /* used as rgb(var(--ml-line) / .07) */
  --ml-inset: 16 15 13; /* inputs, inset panels (bg-inset) */

  /* ---- Glass tokens kept for back-compat; flattened (no blur) ---- */
  --ml-glass-bg: rgb(23 21 19);
  --ml-glass-border: rgba(255, 255, 255, 0.07);
  --ml-glass-blur: 0px;

  /* ---- Brand: electric violet — the ONLY decorative accent ---- */
  --ml-primary: 157 92 255;
  --ml-primary-light: 188 147 255;
  --ml-primary-dark: 124 58 237;

  /* ---- Accent + glow collapsed onto violet (single-accent rule) ---- */
  --ml-accent: 157 92 255;
  --ml-accent-light: 188 147 255;
  --ml-accent-dark: 124 58 237;
  --ml-glow-purple: 157 92 255;
  --ml-glow-pink: 188 147 255;

  /* ---- Semantic (small, rare) ---- */
  --ml-success: 52 211 153; /* live */
  --ml-warning: 244 183 64;
  --ml-danger: 248 113 113;

  /* ---- Typography — families (Unbounded / Onest / JetBrains Mono) ---- */
  --ml-font-sans: 'Onest', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --ml-font-display: 'Unbounded', system-ui, sans-serif;
  --ml-font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'SF Mono', 'Menlo', monospace;

  /* ---- Typography — role tokens.
     5 roles: display, heading-1..4, body, body-emphasis, mono.
     Tightened toward the Console scale (display headings are crisper). ---- */
  --ml-text-display-size: 2.5rem;
  --ml-text-display-weight: 600;
  --ml-text-display-line-height: 1.08;
  --ml-text-display-tracking: -0.02em;

  --ml-text-heading-1-size: 1.875rem;
  --ml-text-heading-1-weight: 600;
  --ml-text-heading-1-line-height: 1.12;
  --ml-text-heading-1-tracking: -0.01em;

  --ml-text-heading-2-size: 1.375rem;
  --ml-text-heading-2-weight: 600;
  --ml-text-heading-2-line-height: 1.2;
  --ml-text-heading-2-tracking: -0.01em;

  --ml-text-heading-3-size: 1.125rem;
  --ml-text-heading-3-weight: 600;
  --ml-text-heading-3-line-height: 1.25;
  --ml-text-heading-3-tracking: -0.005em;

  --ml-text-heading-4-size: 1rem;
  --ml-text-heading-4-weight: 600;
  --ml-text-heading-4-line-height: 1.3;
  --ml-text-heading-4-tracking: 0;

  --ml-text-body-size: 0.875rem;
  --ml-text-body-weight: 400;
  --ml-text-body-line-height: 1.55;
  --ml-text-body-tracking: 0;

  --ml-text-body-emphasis-size: 0.9375rem;
  --ml-text-body-emphasis-weight: 500;
  --ml-text-body-emphasis-line-height: 1.5;
  --ml-text-body-emphasis-tracking: 0;

  --ml-text-mono-size: 0.8125rem;
  --ml-text-mono-weight: 500;
  --ml-text-mono-line-height: 1.45;
  --ml-text-mono-tracking: 0.02em;

  /* ---- Border radius (crisper than the old rounded-2xl) ---- */
  --ml-radius-sm: 0.4375rem; /* 7px */
  --ml-radius-md: 0.625rem; /* 10px */
  --ml-radius-lg: 0.875rem; /* 14px */
  --ml-radius-xl: 1.25rem; /* 20px */
  --ml-radius-full: 9999px;

  /* ---- Shadows — restrained, no neon halos ---- */
  --ml-shadow-surface:
    0 1px 2px rgba(0, 0, 0, 0.45),
    inset 0 0 0 1px rgba(255, 255, 255, 0.07);
  --ml-shadow-surface-hover:
    0 12px 32px -18px rgba(0, 0, 0, 0.85),
    inset 0 0 0 1px rgba(255, 255, 255, 0.11);
  /* glow-* tokens repointed to soft elevation so neon disappears everywhere */
  --ml-shadow-glow: 0 12px 32px -18px rgba(0, 0, 0, 0.8);
  --ml-shadow-glow-lg: 0 24px 56px -24px rgba(0, 0, 0, 0.85);
  --ml-shadow-glow-accent: 0 12px 32px -18px rgba(0, 0, 0, 0.8);

  /* ---- Transitions ---- */
  --ml-transition-fast: 150ms ease;
  --ml-transition-normal: 200ms ease;
  --ml-transition-slow: 300ms ease;

  /* ---- Animation durations ---- */
  --ml-anim-fast: 200ms;
  --ml-anim-normal: 300ms;
  --ml-anim-slow: 500ms;

  /* ---- Sheen — retained tokens; sheen classes are neutralized in CSS ---- */
  --ml-sheen-subtle-duration: 1100ms;
  --ml-sheen-subtle-amplitude: 0.5;
  --ml-sheen-subtle-easing: ease-out;
  --ml-sheen-standard-duration: 900ms;
  --ml-sheen-standard-amplitude: 0.85;
  --ml-sheen-standard-easing: ease-out;
  --ml-sheen-pronounced-duration: 700ms;
  --ml-sheen-pronounced-amplitude: 1;
  --ml-sheen-pronounced-easing: cubic-bezier(0.22, 1, 0.36, 1);

  /* ---- Layout ---- */
  --ml-header-height: 3.625rem; /* 58px — Console topbar */

  /* ============================================================
     MemeLab Console — alias layer.
     The design handoff (prototype HTML/CSS) names tokens like
     --violet / --line / --fill-1 / --bg-raised. Aliasing them here
     (1:1 with the --ml-* values above) lets service screens use the
     handoff's exact class strings verbatim during the per-service
     redesign, instead of translating every token by hand. They drop
     straight into `color`/`background` and Tailwind arbitrary values
     like `text-[var(--violet)]`.

     NOT literal hex any more: `--violet` resolves through the
     `--ml-primary` triplet, and the hover/tint/hairline resolve through
     `color-mix()`. Any CSS use is unaffected — these are valid
     `<color>` values. But JavaScript reading them back with
     `getComputedStyle().getPropertyValue()` gets a `color-mix(...)` or
     `oklab(...)` token stream, NOT `#rrggbb`. Parse a colour out of a
     rendered element instead of out of the custom property.
     Dark theme only for now; the [data-theme="light"] block lands later.
     ============================================================ */
  --bg: #0d0c0b;
  --bg-raised: #171513;
  --bg-inset: #100f0d;
  --bg-hover: #1f1c19;

  --line: rgba(255, 255, 255, 0.07);
  --line-2: rgba(255, 255, 255, 0.13);

  --fill-faint: rgba(255, 255, 255, 0.02);
  --fill-1: rgba(255, 255, 255, 0.055);
  --fill-2: rgba(255, 255, 255, 0.09);
  --edge-hi: rgba(255, 255, 255, 0.1);
  --scrim: rgba(255, 255, 255, 0.1);
  --scrim-2: rgba(255, 255, 255, 0.18);
  --switch-track: rgba(255, 255, 255, 0.12);
  --input-focus-bg: #0b0b11;
  --topbar-bg: rgba(13, 12, 11, 0.82);
  --placeholder: #15151c;
  --avatar-bg: #20202a;

  --fg-1: #f6f4f1;
  --fg: var(--fg-1); /* alias — design markup uses bare --fg for primary text */
  --fg-2: rgb(var(--ml-fg-2)); /* #b0aba3 */
  /* The quiet text token. It was #77726a, which measures 4.09:1 on `--bg`,
     3.82:1 on `--bg-raised` and 3.23:1 by `--surface-200` — under SC 1.4.3's
     4.5:1 everywhere it is actually drawn, and it carries readable text:
     placeholders, timestamps, table captions, sidebar group headings. The
     system's quietest voice was the one nobody could read.
     Today's value keeps the warm hue and the fg-1 > fg-2 > fg-3 order while
     clearing 4.5:1 on every surface in the set. The token is also used for
     chevrons, nav glyphs and status dots, which only ever needed 3:1 — those
     simply get a little clearer.
     Keep this comment in step with `--ml-fg-3` above. It read «#918c82 …
     5.84 / 5.44» for a while after the value had already been nudged up, and
     both the neuroslop skill and a memory note copied those stale numbers out
     of here — a comment about a colour is read as the colour. */
  --fg-3: rgb(var(--ml-fg-3)); /* #979288 — 6.31:1 on --bg, 5.88 on --bg-raised */

  /* ---- Accent: ONE stored value, the rest derived ----
     `--violet` is the only accent colour anybody writes. The hover,
     tint and hairline are computed from it in the ACCENT DERIVATION
     block at the end of this file, so setting `--violet` alone — from
     settings, from a streamer's own colour, from an inline style —
     moves the whole set together instead of leaving a violet border
     around a green button.
     The literals below are the fallback for engines without
     `color-mix()` (older CEF inside OBS still renders our overlays);
     they are exactly today's shipped colours, not a second source of
     truth. `--violet-ink` is the text drawn ON the accent.

     It used to be left undefined so consumers fell through to
     `var(--violet-ink, #fff)`, which kept rendering unchanged until an
     accent supplied its own. That default broke the very contract the
     accent system exists to keep: white on the default violet measures
     **3.88:1**, under SC 1.4.3's 4.5. So the base now carries the ink
     `resolveAccent` itself would choose for this fill — warm charcoal,
     one of its two candidate inks — which measures 5.03:1 on the fill
     and more on the lighter hover. Not a new colour and not a taste
     call: the same answer, applied to the case nobody ran through the
     resolver. */
  --violet: rgb(var(--ml-primary)); /* was #9d5cff — same bytes as 157 92 255 */
  --violet-ink: #0d0c0b;
  --violet-hi: #bc93ff;
  --violet-tint: rgba(157, 92, 255, 0.15);
  --violet-line: rgba(157, 92, 255, 0.42);

  /* Semantics carry a SET, not a single colour — the same shape the accent
     has. Before this, `--live/--warn/--danger` were one flat value each, so
     anything needing a warn *surface*, a warn *hairline* or readable warn
     *text* had to invent it: the chatbot dashboard alone built 244 of them
     out of raw Tailwind (`bg-[color:var(--warn-tint)]` + `text-[color:var(--warn-hi)]` +
     `border-[color:var(--warn-line-soft)]`), which is a literal `#fcd34d` that ignores the
     theme and drifts from `--warn` by design.

     `-hi`   readable text in the semantic hue, drawn on the page ground
     `-tint` the surface a semantic badge or row sits on
     `-line` its hairline
     `-ink`  text drawn ON the solid semantic fill

     Measured, not eyeballed. On dark, `-hi` on `--bg` / `--bg-raised`:
     live 12.70/11.83, warn 13.28/12.38, danger 10.09/9.40. The ink is warm
     charcoal because these three fills are all light: white on them measures
     1.80-2.77, a straight SC 1.4.3 failure, while charcoal gives 7.07-10.88.
     On paper the ink flips to the page itself (4.68-4.99); charcoal there
     would measure 3.47-3.71 and fail. Literals below are the pre-color-mix()
     fallback (old CEF in OBS) — the derivation lives in the block at the end
     of this file. */
  --live: #34d399;
  --live-hi: #89e2b9;
  --live-tint-faint: rgba(52, 211, 153, 0.06);
  --live-tint: rgba(52, 211, 153, 0.15);
  --live-tint-strong: rgba(52, 211, 153, 0.24);
  --live-line-soft: rgba(52, 211, 153, 0.2);
  --live-line: rgba(52, 211, 153, 0.42);
  --live-ink: #0d0c0b;

  --warn: #f4b740;
  --warn-hi: #f8cf88;
  --warn-tint-faint: rgba(244, 183, 64, 0.06);
  --warn-tint: rgba(244, 183, 64, 0.15);
  --warn-tint-strong: rgba(244, 183, 64, 0.24);
  --warn-line-soft: rgba(244, 183, 64, 0.2);
  --warn-line: rgba(244, 183, 64, 0.42);
  --warn-ink: #0d0c0b;

  --danger: #f87171;
  --danger-hi: #ffa19e;
  --danger-tint-faint: rgba(248, 113, 113, 0.06);
  --danger-tint: rgba(248, 113, 113, 0.15);
  --danger-tint-strong: rgba(248, 113, 113, 0.24);
  --danger-line-soft: rgba(248, 113, 113, 0.2);
  --danger-line: rgba(248, 113, 113, 0.42);
  --danger-ink: #0d0c0b;

  /* Accent parity: the same rungs off `--violet`, so a call site never has to
     choose between "on the system" and "the weight this row needs". The two
     the accent already shipped (`--violet-tint`, `--violet-line`) keep their
     values — moving them would move every accented surface in the product. */
  --violet-tint-faint: rgba(157, 92, 255, 0.06);
  --violet-tint-strong: rgba(157, 92, 255, 0.24);
  --violet-line-soft: rgba(157, 92, 255, 0.2);

  --r-sm: 7px;
  --r: 10px;
  --r-lg: 14px;
  --r-xs: 5px; /* chips and tags — below --r-sm they read as pills */
  --r-full: 9999px; /* fully round at any size; 99px / 999px / 9999px were all in use */

  --shadow: 0 12px 32px -18px rgba(0, 0, 0, 0.8);
  --shadow-lg: 0 32px 70px -28px rgba(0, 0, 0, 0.85);

  --font-display: var(--ml-font-display);
  --font-body: var(--ml-font-sans);
  --font-mono: var(--ml-font-mono);
}

/* ============================================================
   LIGHT THEME — warm paper, same brand violet (darkened on light).
   Activated by [data-theme="light"] on <html>. Overrides BOTH the
   --ml-* token layer (so preset Tailwind colors + lib components flip)
   AND the Console alias layer (so .ml-* screens flip). Values from the
   design handoff.
   ============================================================ */

[data-theme='light'] {
  /* --ml-* layer (space-separated RGB for Tailwind alpha) */
  --ml-bg: 244 241 236;
  --ml-fg: 28 24 19;
  --ml-fg-2: 90 82 71;
  /* 112 105 94, not 115 108 97: the light theme sat at 4.48:1 on the same
     --fill-2 ground the dark one failed. Same three-percent correction. */
  --ml-fg-3: 112 105 94;
  --ml-surface-0: 244 241 236;
  --ml-surface-50: 255 255 255;
  --ml-surface-100: 237 232 225;
  --ml-surface-200: 232 226 217;
  --ml-surface-300: 220 213 203;
  --ml-surface-400: 205 197 186;
  --ml-line: 40 32 22;
  --ml-inset: 237 232 225;
  --ml-glass-bg: rgb(255 255 255);
  --ml-glass-border: rgba(40, 32, 22, 0.1);
  --ml-primary: 124 58 237;
  --ml-primary-light: 109 40 217;
  --ml-primary-dark: 91 33 182;
  --ml-accent: 124 58 237;
  --ml-accent-light: 109 40 217;
  --ml-accent-dark: 91 33 182;
  --ml-glow-purple: 124 58 237;
  --ml-glow-pink: 109 40 217;
  --ml-success: 15 157 88;
  --ml-warning: 185 121 0;
  --ml-danger: 217 59 54;
  --ml-shadow-surface:
    0 1px 2px rgba(40, 32, 22, 0.12),
    inset 0 0 0 1px rgba(40, 32, 22, 0.08);
  --ml-shadow-surface-hover:
    0 12px 28px -18px rgba(40, 32, 22, 0.2),
    inset 0 0 0 1px rgba(40, 32, 22, 0.1);
  --ml-shadow-glow: 0 12px 28px -18px rgba(40, 32, 22, 0.18);
  --ml-shadow-glow-lg: 0 24px 52px -24px rgba(40, 32, 22, 0.22);
  --ml-shadow-glow-accent: 0 12px 28px -18px rgba(40, 32, 22, 0.18);

  /* Console alias layer (literal hex/rgba) */
  --bg: #f4f1ec;
  --bg-raised: #ffffff;
  --bg-inset: #ede8e1;
  --bg-hover: #e8e2d9;
  --line: rgba(40, 32, 22, 0.1);
  --line-2: rgba(40, 32, 22, 0.16);
  --fill-faint: rgba(40, 32, 22, 0.022);
  --fill-1: rgba(40, 32, 22, 0.045);
  --fill-2: rgba(40, 32, 22, 0.075);
  --edge-hi: rgba(255, 255, 255, 0);
  --scrim: rgba(40, 32, 22, 0.16);
  --scrim-2: rgba(40, 32, 22, 0.26);
  --switch-track: rgba(40, 32, 22, 0.18);
  --input-focus-bg: #ffffff;
  --topbar-bg: rgba(244, 241, 236, 0.85);
  --placeholder: #e7e1d8;
  --avatar-bg: #e7e1d8;
  --fg-1: #1c1813;
  --fg: var(--fg-1);
  --fg-2: rgb(var(--ml-fg-2)); /* #5a5247 */
  /* Paper fails harder than charcoal did: #8c8478 measured 3.28:1 on `--bg`
     and 3.69:1 on white. Same rule as dark — keep the hex here in step with
     `--ml-fg-3` above. */
  --fg-3: rgb(var(--ml-fg-3)); /* #70695e — 4.82:1 on --bg, 5.42 on white */
  /* Same rule as dark: only `--violet` is stored. On paper the hover
     darkens instead of lightening — see the derivation block below. */
  --violet: rgb(var(--ml-primary)); /* was #7c3aed — same bytes as 124 58 237 */
  /* The ink has to be re-derived here, and it was not. Warm charcoal was picked
     for the DARK fill (#9d5cff, where it measures 5.03:1); on paper the accent
     is DARKER (#7c3aed) and the same charcoal drops to **3.43:1** — on every
     primary action of every service, since seven rules in console.css read this
     token. White gives 5.70:1 here.
     The irony worth keeping: this token exists BECAUSE of this exact defect. It
     was added when `var(--violet-ink, #fff)` was found to give 3.88:1 on the
     dark accent, and that fix was applied to one theme. Do not delete either
     declaration on the grounds that the other theme already has one. */
  --violet-ink: #ffffff;
  --violet-hi: #6d28d9;
  --violet-tint: rgba(124, 58, 237, 0.1);
  --violet-line: rgba(124, 58, 237, 0.3);
  /* Semantics carry status WORDS — «Активен», «Выключен», «Ошибка» — so they
     are held to SC 1.4.3's 4.5:1 like any other text, not to the 3:1 that
     would apply if they were only dots and fills. On paper the old set missed
     it badly: live 3.11, warn 3.22, danger 4.03 against `--bg`. Darkened just
     far enough to clear the bar on both paper surfaces, hue unchanged:
       live   4.68 / 5.27
       warn   4.99 / 5.62
       danger 4.90 / 5.52
     Dark-theme semantics already pass comfortably (9.5-10.9) and are untouched. */
  --live: #0b7c45;
  --live-hi: #096e3d;
  /* Paper needs LOWER weights than dark does, and the reason is not taste.
     On dark a tint lightens the ground and pushes text contrast up; on paper
     the same tint is a DARK wash that pulls the ground down toward the ink,
     and the ink here is dark too. Measured across every semantic x rung x
     surface: at the dark theme's 15/24 the light theme lands at 4.22-4.07,
     under SC 1.4.3. At 8/14 the worst case is 4.54. */
  --live-tint-faint: rgba(11, 124, 69, 0.04);
  --live-tint: rgba(11, 124, 69, 0.08);
  --live-tint-strong: rgba(11, 124, 69, 0.14);
  --live-line-soft: rgba(11, 124, 69, 0.2);
  --live-line: rgba(11, 124, 69, 0.30);
  /* Paper ink: these three fills are dark on light, so the text on them is
     the page, not charcoal — 4.68 / 4.99 / 4.90 against 3.71 / 3.47 / 3.54. */
  --live-ink: #f4f1ec;

  --warn: #8f5d00;
  --warn-hi: #7f5200;
  --warn-tint-faint: rgba(143, 93, 0, 0.04);
  --warn-tint: rgba(143, 93, 0, 0.08);
  --warn-tint-strong: rgba(143, 93, 0, 0.14);
  --warn-line-soft: rgba(143, 93, 0, 0.2);
  --warn-line: rgba(143, 93, 0, 0.30);
  --warn-ink: #f4f1ec;

  --danger: #c4302b;
  --danger-hi: #af2a25;
  --danger-tint-faint: rgba(196, 48, 43, 0.04);
  --danger-tint: rgba(196, 48, 43, 0.08);
  --danger-tint-strong: rgba(196, 48, 43, 0.14);
  --danger-line-soft: rgba(196, 48, 43, 0.2);
  --danger-line: rgba(196, 48, 43, 0.30);
  --danger-ink: #f4f1ec;

  --violet-tint-faint: rgba(124, 58, 237, 0.04);
  --violet-tint-strong: rgba(124, 58, 237, 0.14);
  --violet-line-soft: rgba(124, 58, 237, 0.2);
  --shadow: 0 12px 28px -18px rgba(40, 32, 22, 0.18);
  --shadow-lg: 0 30px 64px -30px rgba(40, 32, 22, 0.22);
}

[data-theme='light'] select {
  color-scheme: light;
}

/* ============================================================
   ACCENT DERIVATION — the single-variable rule.

   Everything violet is computed from `--violet`. Set that one variable
   and the hover, tint and hairline follow; nothing else has to be
   written by hand, and a streamer's own colour cannot leave half the
   accent behind.

   Mixing happens in OKLab, so the hover keeps its hue at any input
   colour instead of drifting the way an sRGB mix does. The percentages
   are fitted to the colours we already ship: on dark the hover lands
   within 4/765 of `#bc93ff`, on light within 17/765 of `#6d28d9` —
   below the threshold of sight, so this refactor changes no pixel of
   the default theme while making every other colour work. Measured on
   real pixels: dark hover off by 2/255, dark tint by 3, dark hairline
   by 1; light hover by 11 on green, the rest under 4.

   That light-hover 11 is a deliberate trade, not a miss. `#6d28d9` is a
   Tailwind stop, not a lightness step off `#7c3aed`, so no single mix
   reproduces it. Mixing toward a dark violet would close the gap to 8 —
   and would tint a streamer's green hover purple, which is the exact bug
   this block exists to remove. Black is hue-neutral, so black it is.

   `transparent` is `rgb(0 0 0 / 0)` and `color-mix()` premultiplies, so
   mixing toward it yields the accent at that alpha with the hue intact
   — exactly what the rgba() literals above spelled out by hand.

   Wrapped in @supports on purpose: engines without `color-mix()` keep
   the literals declared above, which are today's exact values. Do NOT
   move these declarations up into the theme blocks — the literals are
   the fallback and must stay separately declared for that to work.

   Scope matters, and it bites: a derived custom property is computed on
   the element that DECLARES it. Setting `--violet` on some nested div
   does not recompute a `--violet-hi` declared on `:root` — the subtree
   would inherit the root's hover over its own accent. So the rules are
   declared for `:root` AND for any element carrying `data-accent`: put
   that attribute on whatever element you scope an accent to (a settings
   preview, a per-channel panel) and the whole set recomputes there.
   Without the attribute, only the root accent is live.

   ONE stored value for the whole library: `--ml-primary`. React
   components and the Tailwind preset already read that triplet;
   `--violet` now resolves from it, and the hover/tint/hairline resolve
   from `--violet`. So a custom accent is written in exactly one place.
   It has to be the triplet form (`r g b`, space-separated) because
   Tailwind's alpha syntax needs it and no CSS function produces that
   form from a hex — converting is one line in whatever applies it.

   Two gaps the applier MUST handle, because CSS cannot:

   1. Old engines. `@supports` protects only the DEFAULT accent: on an
      engine without `color-mix()` a custom `--ml-primary` turns the fill
      green while the literals above keep the hover and hairline violet.
      Before applying a custom accent, check
      `CSS.supports('color', 'color-mix(in oklab, red 50%, white)')` and,
      when false, set all four values explicitly.
   2. Invalid input. A malformed `--ml-primary` makes every dependent
      `color-mix()` invalid at computed-value time, and the cascade does
      NOT fall back to the literals above — it falls back to `unset`.
      Validate and clamp the colour before it ever reaches the DOM.
   ============================================================ */

/* Link 1 of the chain, outside @supports because rgb() needs no guard:
   an accent scope re-resolves --violet from ITS OWN --ml-primary. Without
   this the subtree would inherit the root's violet and only the fills fed
   straight from the triplet would turn. */

[data-accent] {
  --violet: rgb(var(--ml-primary));
}

/* Links 2-4: hover, tint and hairline off whatever --violet resolved to. */

@supports (color: color-mix(in oklab, red 50%, white)) {
  :root,
  [data-accent] {
    --violet-hi: color-mix(in oklab, var(--violet) 68%, #fff);
    --violet-tint: color-mix(in oklab, var(--violet) 15%, transparent);
    --violet-line: color-mix(in oklab, var(--violet) 42%, transparent);
  }

  /* Paper ground: the hover goes darker, and the tint/hairline sit
     lighter than on dark because the surface reflects instead of
     absorbing. Same single input. */
  [data-theme='light'],
  [data-theme='light'] [data-accent] {
    --violet-hi: color-mix(in oklab, var(--violet) 92%, #000);
    --violet-tint: color-mix(in oklab, var(--violet) 10%, transparent);
    --violet-line: color-mix(in oklab, var(--violet) 30%, transparent);
  }
}

/* ============================================================
   SEMANTIC DERIVATION — same single-variable rule, three more inputs.

   `--live`, `--warn` and `--danger` each drive their own set, so the three
   semantics and the accent behave identically and a service that overrides one
   semantic colour gets the whole set with it.

   The set, and what each member is FOR — the split is the point, because the
   defect this replaces was one flat colour being asked to be all six:

     --x            the fill itself: dots, bars, knobs, meter segments
     --x-hi         ANY semantic text, on any of the surfaces below
     --x-ink        text drawn ON the solid --x fill
     --x-tint-faint / --x-tint / --x-tint-strong    surfaces, three weights
     --x-line-soft / --x-line                       hairlines, two weights

   Two rules that came out of measuring rather than taste:

   1. There is no quiet rung for semantic text. A dimmed `-hi` fails SC 1.4.3
      the moment it lands on a tinted surface — 3.51 at worst — and the light
      theme has no headroom at all (2.40-4.10 across the authored range it
      replaced). Semantic hue and quietness are different axes: when text needs
      to be quieter, it takes `--fg-2` / `--fg-3` and keeps the hue for the
      thing that IS the state.
   2. `--x` is not a text colour. As text on its own tint it measures 4.22 on
      paper. Text is `-hi`, always.

   Deliberately NOT scoped to [data-accent]: a streamer's accent is theirs to
   pick, but "this is broken" is not. Semantics stay the platform's.

   `-ink` is absent here on purpose — it is not a mix of its own colour but
   whichever ground is readable ON that fill, and that flips between themes
   rather than sliding. It stays a literal in each theme block.
   ============================================================ */

@supports (color: color-mix(in oklab, red 50%, white)) {
  :root {
    --live-hi: color-mix(in oklab, var(--live) 68%, #fff);
    --live-tint-faint: color-mix(in oklab, var(--live) 6%, transparent);
    --live-tint: color-mix(in oklab, var(--live) 15%, transparent);
    --live-tint-strong: color-mix(in oklab, var(--live) 24%, transparent);
    --live-line-soft: color-mix(in oklab, var(--live) 20%, transparent);
    --live-line: color-mix(in oklab, var(--live) 42%, transparent);

    --warn-hi: color-mix(in oklab, var(--warn) 68%, #fff);
    --warn-tint-faint: color-mix(in oklab, var(--warn) 6%, transparent);
    --warn-tint: color-mix(in oklab, var(--warn) 15%, transparent);
    --warn-tint-strong: color-mix(in oklab, var(--warn) 24%, transparent);
    --warn-line-soft: color-mix(in oklab, var(--warn) 20%, transparent);
    --warn-line: color-mix(in oklab, var(--warn) 42%, transparent);

    --danger-hi: color-mix(in oklab, var(--danger) 68%, #fff);
    --danger-tint-faint: color-mix(in oklab, var(--danger) 6%, transparent);
    --danger-tint: color-mix(in oklab, var(--danger) 15%, transparent);
    --danger-tint-strong: color-mix(in oklab, var(--danger) 24%, transparent);
    --danger-line-soft: color-mix(in oklab, var(--danger) 20%, transparent);
    --danger-line: color-mix(in oklab, var(--danger) 42%, transparent);
  }

  :root,
  [data-accent] {
    --violet-tint-faint: color-mix(in oklab, var(--violet) 6%, transparent);
    --violet-tint-strong: color-mix(in oklab, var(--violet) 24%, transparent);
    --violet-line-soft: color-mix(in oklab, var(--violet) 20%, transparent);
  }

  /* Paper: `-hi` goes darker, and the tints sit LIGHTER than on dark — not
     for symmetry but because on paper a tint is a dark wash that pulls the
     ground toward the (also dark) ink. Carrying the dark theme's 15/24 across
     puts `-hi` on a strong tint at 4.07; 8/14 holds the worst case at 4.54. */
  [data-theme='light'] {
    --live-hi: color-mix(in oklab, var(--live) 92%, #000);
    --live-tint-faint: color-mix(in oklab, var(--live) 4%, transparent);
    --live-tint: color-mix(in oklab, var(--live) 8%, transparent);
    --live-tint-strong: color-mix(in oklab, var(--live) 14%, transparent);
    --live-line-soft: color-mix(in oklab, var(--live) 20%, transparent);
    --live-line: color-mix(in oklab, var(--live) 30%, transparent);

    --warn-hi: color-mix(in oklab, var(--warn) 92%, #000);
    --warn-tint-faint: color-mix(in oklab, var(--warn) 4%, transparent);
    --warn-tint: color-mix(in oklab, var(--warn) 8%, transparent);
    --warn-tint-strong: color-mix(in oklab, var(--warn) 14%, transparent);
    --warn-line-soft: color-mix(in oklab, var(--warn) 20%, transparent);
    --warn-line: color-mix(in oklab, var(--warn) 30%, transparent);

    --danger-hi: color-mix(in oklab, var(--danger) 92%, #000);
    --danger-tint-faint: color-mix(in oklab, var(--danger) 4%, transparent);
    --danger-tint: color-mix(in oklab, var(--danger) 8%, transparent);
    --danger-tint-strong: color-mix(in oklab, var(--danger) 14%, transparent);
    --danger-line-soft: color-mix(in oklab, var(--danger) 20%, transparent);
    --danger-line: color-mix(in oklab, var(--danger) 30%, transparent);
  }

  [data-theme='light'],
  [data-theme='light'] [data-accent] {
    --violet-tint-faint: color-mix(in oklab, var(--violet) 4%, transparent);
    --violet-tint-strong: color-mix(in oklab, var(--violet) 14%, transparent);
    --violet-line-soft: color-mix(in oklab, var(--violet) 20%, transparent);
  }
}

/**
 * Material semantic tokens.
 *
 * 4-material ontology for @memelabui/ui.
 * Namespace: --ml-mat-{material}-{property}
 *
 * Materials:
 *   surface    — standard card / content area
 *   instrument — interactive control panel (buttons, sliders, knobs)
 *   console    — dense data table / sidebar / terminal
 *   marquee    — display / hero / spotlight
 *
 * Properties per material:
 *   bg      — background (space-separated RGB channels for Tailwind opacity modifier)
 *   border  — border color (space-separated RGB channels)
 *   blur    — backdrop-filter blur value
 *   shadow  — box-shadow value
 *   ring    — focus ring / outline value
 *
 * ── Why every value here is a `var()` and not a literal ────────────────────
 *
 * Until 0.49.0 this file held its own copy of the pre-Console palette: cool
 * blue-blacks (`20 20 32`, `26 26 46`, `36 36 58`), backdrop blurs of 16/12/24px,
 * and four different ring hues, none of them the platform accent. Meanwhile the
 * `.mat-*` classes in `styles/components.css` had been flattened to the warm
 * charcoal ladder. So the two halves of one named material disagreed: the
 * Tailwind utility `bg-mat-surface` painted cool `20 20 32` while the class
 * `.mat-surface` painted warm `23 21 19`, and on paper the utility stayed dark
 * because these literals carried no light-theme override at all.
 *
 * Nothing consumed the utilities, so nobody saw it — a trap, not a bug, and the
 * kind that only fires on whoever adopts the API first. Deriving from the
 * ladder makes the two halves the same value by construction, and the light
 * theme follows for free because `--ml-surface-*` already flips there.
 *
 * Keep each rung in step with its class in `styles/components.css`.
 */

:root {
  /* ── surface ─────────────────────────────────────────────────────────────
     Standard card / content area. Same rung as `.panel` and `.mat-surface`. */
  --ml-mat-surface-bg: var(--ml-surface-50);
  --ml-mat-surface-border: var(--ml-line);
  --ml-mat-surface-blur: 0px;
  --ml-mat-surface-shadow: var(--ml-shadow-surface);
  --ml-mat-surface-ring: rgb(var(--ml-primary) / 0.45);

  /* ── instrument ──────────────────────────────────────────────────────────
     Interactive control panel — buttons, sliders, toggle rows, knobs.
     One rung above surface, which is what carries the affordance.          */
  --ml-mat-instrument-bg: var(--ml-surface-100);
  --ml-mat-instrument-border: var(--ml-line);
  --ml-mat-instrument-blur: 0px;
  --ml-mat-instrument-shadow: var(--ml-shadow-surface);
  --ml-mat-instrument-ring: rgb(var(--ml-primary) / 0.45);

  /* ── console ─────────────────────────────────────────────────────────────
     Dense data table / sidebar / terminal / log viewer. The inset rung —
     deepest in dark, and correctly the paper-tinted one in light.          */
  --ml-mat-console-bg: var(--ml-inset);
  --ml-mat-console-border: var(--ml-line);
  --ml-mat-console-blur: 0px;
  --ml-mat-console-shadow: var(--ml-shadow-surface);
  --ml-mat-console-ring: rgb(var(--ml-primary) / 0.45);

  /* ── marquee ─────────────────────────────────────────────────────────────
     Display / hero / spotlight / lower-third. The most raised rung; it reads
     as emphasis through elevation, not through luminosity or blur.         */
  --ml-mat-marquee-bg: var(--ml-surface-200);
  --ml-mat-marquee-border: var(--ml-line);
  --ml-mat-marquee-blur: 0px;
  --ml-mat-marquee-shadow: var(--ml-shadow-surface);
  --ml-mat-marquee-ring: rgb(var(--ml-primary) / 0.45);
}

/**
 * Motion intent tokens — PR-0.3
 *
 * 6-intent motion semantic ontology for @memelabui/ui.
 * Namespace: --ml-motion-{intent}-{duration|easing}
 *
 * Intents (Charter Rule 6 — fixed, never extend):
 *   enter     — first appearance (page load, modal open)       smooth deceleration
 *   exit      — removal (toast dismiss, modal close)           snappy acceleration
 *   emphasize — attention on change (badge update, error)      slight overshoot
 *   reorder   — list rearrangement (FLIP, layout)              material ease-out
 *   feedback  — response to user action (click, hover)         sharp linear-out
 *   ambient   — background motion without trigger (orbs, sheen) slow loop-friendly
 *
 * All values are additive — zero existing tokens modified.
 * Nobody consumes these yet; adoption is opt-in (PR-1.2a Motion Intent API).
 *
 * Value rationale:
 *   enter     240ms — comfortable deceleration for appearing elements; not too slow for modals
 *   exit      160ms — faster than enter; exits should feel snappy, not linger
 *   emphasize 320ms — longer than enter to let the overshoot register; spring feel
 *   reorder   280ms — mid-range; FLIP animations need time to read spatial change
 *   feedback  120ms — near-instant; click/hover response must feel immediate
 *   ambient   4000ms — slow enough to be subliminal; loop-friendly (divisible by common RAF)
 */

:root {
  /* ── enter ───────────────────────────────────────────────────────────────
     First appearance: page load, modal open, drawer slide-in.
     Smooth deceleration — element arrives with authority, not abruptly.
     cubic-bezier: fast start, gentle settle (material decelerate curve).   */
  --ml-motion-enter-duration: 240ms;
  --ml-motion-enter-easing: cubic-bezier(0.0, 0.0, 0.2, 1);

  /* ── exit ────────────────────────────────────────────────────────────────
     Removal: toast dismiss, modal close, drawer hide.
     Snappy acceleration — element leaves quickly, doesn't overstay.
     cubic-bezier: slow start, fast finish (material accelerate curve).     */
  --ml-motion-exit-duration: 160ms;
  --ml-motion-exit-easing: cubic-bezier(0.4, 0.0, 1, 1);

  /* ── emphasize ───────────────────────────────────────────────────────────
     Attention on change: badge count update, error highlight, live indicator.
     Slight overshoot — draws the eye without being jarring.
     cubic-bezier: spring-like, overshoots slightly past 1 then settles.    */
  --ml-motion-emphasize-duration: 320ms;
  --ml-motion-emphasize-easing: cubic-bezier(0.2, 0.0, 0.0, 1.4);

  /* ── reorder ─────────────────────────────────────────────────────────────
     List rearrangement: FLIP animations, layout shifts, drag-and-drop.
     Material ease-out — spatial movement reads clearly, lands cleanly.
     cubic-bezier: standard material ease-out (fast start, smooth landing). */
  --ml-motion-reorder-duration: 280ms;
  --ml-motion-reorder-easing: cubic-bezier(0.0, 0.0, 0.2, 1);

  /* ── feedback ────────────────────────────────────────────────────────────
     Response to user action: button click, toggle, hover state change.
     Sharp linear-out — must feel immediate; any lag breaks affordance.
     cubic-bezier: near-linear with slight ease-out for polish.             */
  --ml-motion-feedback-duration: 120ms;
  --ml-motion-feedback-easing: cubic-bezier(0.2, 0.0, 0.4, 1);

  /* ── ambient ─────────────────────────────────────────────────────────────
     Background motion without trigger: orbs drift, sheen hover group.
     Slow loop-friendly — subliminal, never competes with content.
     ease-in-out: smooth loop with no abrupt direction change.              */
  --ml-motion-ambient-duration: 4000ms;
  --ml-motion-ambient-easing: cubic-bezier(0.45, 0.0, 0.55, 1);
}

/**
 * Density tokens — PR-0.2
 *
 * Three density modes (compact / comfortable / loose) expressed as CSS-variable
 * deltas under `--ml-density-*` namespace. Activated by `<DensityScope mode>`
 * which sets `data-density="<mode>"` on a wrapping div; selectors below
 * cascade scoped to that subtree.
 *
 * NEVER applied to <html> or <body> globally — composition (e.g. compact
 * sidebar inside a comfortable page) requires subtree scoping.
 *
 * Tokens cover spacing (xs..xl) and text-size (xs..lg). Components consuming
 * these vars adopt density automatically when wrapped. Adoption in existing
 * components is opt-in and phased — Phase 0 ships only the API.
 *
 * Default is `comfortable` — both for `:root` (no scope) and inside
 * `[data-density="comfortable"]` for cascade clarity.
 */

:root {
  /* Default = comfortable — applies when no <DensityScope> is mounted. */
  --ml-density-spacing-xs: 0.25rem;   /*  4px */
  --ml-density-spacing-sm: 0.5rem;    /*  8px */
  --ml-density-spacing-md: 1rem;      /* 16px */
  --ml-density-spacing-lg: 1.5rem;    /* 24px */
  --ml-density-spacing-xl: 2rem;      /* 32px */

  --ml-density-text-xs: 0.75rem;      /* 12px */
  --ml-density-text-sm: 0.875rem;     /* 14px */
  --ml-density-text-md: 1rem;         /* 16px */
  --ml-density-text-lg: 1.125rem;     /* 18px */
}

/* ── compact ──────────────────────────────────────────────────────────────
   Chat sidebars, data tables, dense panels. Spacing tightened ~50%, text
   pulled in by ~1px to maximize information density without losing legibility. */

[data-density='compact'] {
  --ml-density-spacing-xs: 0.125rem;  /*  2px */
  --ml-density-spacing-sm: 0.25rem;   /*  4px */
  --ml-density-spacing-md: 0.5rem;    /*  8px */
  --ml-density-spacing-lg: 0.875rem;  /* 14px */
  --ml-density-spacing-xl: 1.25rem;   /* 20px */

  --ml-density-text-xs: 0.6875rem;    /* 11px */
  --ml-density-text-sm: 0.8125rem;    /* 13px */
  --ml-density-text-md: 0.9375rem;    /* 15px */
  --ml-density-text-lg: 1.0625rem;    /* 17px */
}

/* ── comfortable ──────────────────────────────────────────────────────────
   Default. Explicit declaration for cascade clarity — equivalent to :root. */

[data-density='comfortable'] {
  --ml-density-spacing-xs: 0.25rem;   /*  4px */
  --ml-density-spacing-sm: 0.5rem;    /*  8px */
  --ml-density-spacing-md: 1rem;      /* 16px */
  --ml-density-spacing-lg: 1.5rem;    /* 24px */
  --ml-density-spacing-xl: 2rem;      /* 32px */

  --ml-density-text-xs: 0.75rem;      /* 12px */
  --ml-density-text-sm: 0.875rem;     /* 14px */
  --ml-density-text-md: 1rem;         /* 16px */
  --ml-density-text-lg: 1.125rem;     /* 18px */
}

/* ── loose ────────────────────────────────────────────────────────────────
   Long-watching sessions, marketing surfaces, documentation. Spacing opened
   up ~30%, text scaled up ~1px for restful reading rhythm. */

[data-density='loose'] {
  --ml-density-spacing-xs: 0.375rem;  /*  6px */
  --ml-density-spacing-sm: 0.75rem;   /* 12px */
  --ml-density-spacing-md: 1.25rem;   /* 20px */
  --ml-density-spacing-lg: 2rem;      /* 32px */
  --ml-density-spacing-xl: 2.75rem;   /* 44px */

  --ml-density-text-xs: 0.8125rem;    /* 13px */
  --ml-density-text-sm: 0.9375rem;    /* 15px */
  --ml-density-text-md: 1.0625rem;    /* 17px */
  --ml-density-text-lg: 1.1875rem;    /* 19px */
}

/*
  Platform identity — the colours that mean "this came from Twitch".

  This layer replaces the palette half of the old channel-DNA tokens
  (`--ml-ch-*-primary` and friends, removed in 0.28.0). Those had no
  consumer anywhere in the library or in any service; what they did have
  was Twitch at `169 116 255` sitting a hair away from the product accent
  at `157 92 255` — two different meanings wearing one colour.

  Two rules keep that from coming back.

  1. Platform colour is METADATA, and metadata never outshouts action.
     Every value here is pinned to chroma 0.11 in OKLCh, against the
     accent's 0.23. A platform chip is visibly desaturated next to a
     primary button, so the two cannot be confused even when the streamer
     has chosen a violet accent. Lightness is then solved for 4.5:1
     against that theme's page — these are text colours, and they owe the
     text threshold.

  2. Identity rides on the LETTERS, not on the hue. `<PlatformMark>` draws
     the platform's short code in the mono face; the colour is a second
     cue and defaults to off. That is why the desaturation costs nothing:
     nothing here is load-bearing for recognition.

  Brand anchors are recorded for provenance only — no brand artwork is
  shipped, and no brand colour is reproduced exactly.
*/

:root {
  /* Twitch — anchor #9146FF, hue 296° */
  --ml-platform-twitch: 136 115 188;
  /* YouTube — anchor #FF0000, hue 29° */
  --ml-platform-youtube: 188 104 92;
  /* VK Video — anchor #0077FF, hue 258° */
  --ml-platform-vkvideo: 85 128 193;
  /* Anything we have no verified anchor for stays neutral rather than invented. */
  --ml-platform-neutral: 176 171 163;
}

[data-theme='light'] {
  --ml-platform-twitch: 114 93 164;
  --ml-platform-youtube: 162 81 70;
  --ml-platform-vkvideo: 63 105 167;
  --ml-platform-neutral: 90 82 71;
}

/* ============================================================
   MemeLab Console — component classes.
   Ported from the design handoff (memelab-ds.css) so service
   screens can be built with the SAME markup/classes as the
   prototype (true 1:1), not reskinned onto old layouts.
   Relies on the Console alias tokens in tokens/tokens.css
   (--violet, --line, --fill-1..3, --bg-raised, --r, --font-display…).
   Do not write a star followed by a slash in here: it ends the comment.
   ============================================================ */

/* ---------- Typography ---------- */

.ml-display {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.12;
  color: var(--fg-1);
  margin: 0;
}

.ml-h1 { font-family: var(--font-display); font-weight: 600; font-size: 30px; letter-spacing: -0.01em; line-height: 1.12; margin: 0; }

.ml-h2 { font-family: var(--font-display); font-weight: 500; font-size: 20px; margin: 0; }

.ml-h3 { font-family: var(--font-display); font-weight: 500; font-size: 15px; margin: 0; }

.ml-body { font-family: var(--font-body); color: var(--fg-2); line-height: 1.62; }

.ml-eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-3);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  white-space: nowrap;
}

.ml-eyebrow .idx { color: var(--violet); }

.ml-eyebrow.live .idx { color: var(--live); }

/* A lamp, not a glyph. `<span class="idx">●</span>` shipped 96 times and
   reported something in 5 of them; the other 91 were ornament and are gone.
   Where it survives it is empty markup drawn by CSS, so the shape no longer
   depends on which font happened to load. No pulse: it reports state, and a
   blinking one would be back to decoration. */

.ml-eyebrow .idx:empty {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: currentColor;
  flex: none;
}

.ml-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }

.ml-mono { font-family: var(--font-mono); }

/* ---------- Surfaces ---------- */

.ml-panel {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  position: relative;
}

.ml-panel::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--edge-hi), transparent);
  border-radius: var(--r) var(--r) 0 0;
}

.ml-panel--flat { box-shadow: none; }

.ml-panel--inset { background: var(--bg-inset); box-shadow: none; }

.ml-panel--inset::before { display: none; }

.ml-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
}

.ml-panel-body { padding: 18px; }

.ml-rule { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ---------- Buttons ---------- */

.ml-btn {
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 38px;
  padding: 0 16px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.14s, border-color 0.14s, color 0.14s, transform 0.06s;
  white-space: nowrap;
  -webkit-user-select: none;
     -moz-user-select: none;
          user-select: none;
}

.ml-btn:active { transform: translateY(0.5px); }

.ml-btn svg { width: 16px; height: 16px; }

.ml-btn--primary { background: var(--violet); color: var(--violet-ink, #fff); }

.ml-btn--primary:hover { background: var(--violet-hi); }

.ml-btn--ghost { background: var(--fill-1); color: var(--fg-1); border-color: var(--line-2); }

.ml-btn--ghost:hover { background: var(--fill-2); }

.ml-btn--quiet { background: transparent; color: var(--fg-2); }

.ml-btn--quiet:hover { background: var(--fill-1); color: var(--fg-1); }

.ml-btn--danger { background: transparent; color: var(--danger-hi); border-color: var(--danger-line); }

.ml-btn--danger:hover { background: var(--danger-tint); }

.ml-btn--sm { height: 31px; padding: 0 12px; font-size: 12.5px; }

.ml-btn--lg { height: 44px; padding: 0 22px; font-size: 15px; }

/* Unavailable, not broken. A control with no disabled state looks pressable and
   answers nothing — the whole console shipped without one. The fill goes flat,
   the ink drops one rung, and hover/press stop replying so the mouse agrees
   with the eye. Variant hovers are silenced explicitly: they are separate
   rules and would otherwise still fire. */

.ml-btn:disabled,
.ml-btn[aria-disabled='true'] {
  cursor: not-allowed;
  background: var(--fill-1);
  border-color: transparent;
  color: var(--fg-3);
  transform: none;
}

.ml-btn:disabled:hover,
.ml-btn[aria-disabled='true']:hover,
.ml-btn--primary:disabled:hover,
.ml-btn--ghost:disabled:hover,
.ml-btn--quiet:disabled:hover,
.ml-btn--danger:disabled:hover {
  background: var(--fill-1);
  color: var(--fg-3);
}

.ml-btn:disabled:active,
.ml-btn[aria-disabled='true']:active { transform: none; }

.ml-btn:disabled svg,
.ml-btn[aria-disabled='true'] svg { opacity: 0.7; }

.ml-switch:disabled { cursor: not-allowed; opacity: 0.45; }

.ml-iconbtn {
  position: relative;
  width: 34px; height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm);
  background: transparent; color: var(--fg-2);
  border: 1px solid transparent; cursor: pointer;
  transition: background 0.14s, color 0.14s;
}

/* The button looks 34px and is HIT at 40. Those are different questions: 34px
   is the right visual weight for a header full of these, and 40px is the floor
   below which a thumb starts missing — the platform's own rule, which every
   icon button on every service was quietly under. Growing the box instead
   would have moved layout in 33 files; growing only the target moves nothing.
   Adjacent buttons sit 8px apart, so two 3px skirts still leave 2px between
   them and no target swallows its neighbour. */

.ml-iconbtn::after {
  content: '';
  position: absolute;
  inset: -3px;
}

.ml-iconbtn:hover { background: var(--fill-2); color: var(--fg-1); }

/* A press has its own face. Without one, holding the button down looked
   exactly like hovering it, so the only control that never confirmed a press
   was the one you press most often. Motion, not a new fill — the same half
   pixel `.ml-btn` uses, because the console should have one press language
   and not two. */

.ml-iconbtn:active { transform: translateY(0.5px); }

.ml-iconbtn svg { width: 17px; height: 17px; }

/* Disabled goes AFTER hover and press, and silences both. Declared above them
   it tied on specificity and lost on source order — an unavailable icon button
   lit up under the pointer exactly like a working one. */

.ml-iconbtn:disabled,
.ml-iconbtn[aria-disabled='true'] {
  cursor: not-allowed;
  color: var(--fg-3);
  background: transparent;
  transform: none;
}

.ml-iconbtn:disabled:hover,
.ml-iconbtn:disabled:active,
.ml-iconbtn[aria-disabled='true']:hover,
.ml-iconbtn[aria-disabled='true']:active {
  background: transparent;
  color: var(--fg-3);
  transform: none;
}

/* ---------- Form controls ---------- */

.ml-field-label { font-size: 12.5px; font-weight: 500; color: var(--fg-2); display: block; margin-bottom: 7px; }

.ml-input, .ml-select, .ml-textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--fg-1);
  background: var(--bg-inset);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  padding: 0 13px;
  height: 40px;
  transition: border-color 0.14s, background 0.14s;
}

.ml-textarea { padding: 11px 13px; height: auto; line-height: 1.55; resize: vertical; }

.ml-input::-moz-placeholder, .ml-textarea::-moz-placeholder { color: var(--fg-3); }

.ml-input::placeholder, .ml-textarea::placeholder { color: var(--fg-3); }

.ml-input:focus, .ml-select:focus, .ml-textarea:focus { outline: none; border-color: var(--violet-line); background: var(--input-focus-bg); }

.ml-select {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2371717F' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; padding-right: 34px;
}

.ml-switch { width: 42px; height: 24px; border-radius: var(--r-full); background: var(--switch-track); border: 0; padding: 0; cursor: pointer; position: relative; transition: background 0.16s; flex: none; }

/* The console's most common control was a 42x24 tap target against a 40px
   floor — five of them on one «Реакции», one per row on «Командах». The track
   stays 24px, which is what makes it read as a switch; the AREA grows to
   42x40 with a transparent extension. Nothing moves on screen. */

.ml-switch::before { content: ''; position: absolute; inset: -8px 0; border-radius: inherit; }

.ml-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: var(--r-full); background: var(--fg-1); transition: transform 0.16s, background 0.16s; }

.ml-switch[aria-pressed='true'] { background: var(--violet); }

.ml-switch[aria-pressed='true']::after { transform: translateX(18px); background: #fff; }

.ml-search { position: relative; }

.ml-search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--fg-3); }

.ml-search .ml-input { padding-left: 36px; }

/* ---------- Badges · chips · status ---------- */

.ml-badge {
  font-family: var(--font-mono);
  font-size: 11px; font-weight: 500; letter-spacing: 0.04em;
  display: inline-flex; align-items: center; gap: 6px;
  height: 22px; padding: 0 9px; border-radius: var(--r-full);
  background: var(--fill-1); color: var(--fg-2);
  border: 1px solid var(--line);
  white-space: nowrap; flex-shrink: 0;
}

.ml-badge--violet { background: var(--violet-tint); color: var(--violet-hi); border-color: var(--violet-line); }

.ml-badge--live { background: var(--live-tint); color: var(--live-hi); border-color: var(--live-line); }

.ml-badge--warn { background: var(--warn-tint); color: var(--warn-hi); border-color: var(--warn-line); }

.ml-badge--danger { background: var(--danger-tint); color: var(--danger-hi); border-color: var(--danger-line); }

.ml-dot { width: 7px; height: 7px; border-radius: var(--r-full); background: var(--fg-3); flex: none; }

.ml-dot--live { background: var(--live); box-shadow: 0 0 0 3px var(--live-tint); }

.ml-dot--off { background: var(--fg-3); }

.ml-dot--warn { background: var(--warn); }

.ml-dot--danger { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-tint); }

.ml-dot--unknown { background: transparent; box-shadow: inset 0 0 0 2px var(--fg-3); }

@media (prefers-reduced-motion: no-preference) {
  .ml-badge--live .ml-dot--live { animation: ml-onair 2.4s ease-in-out infinite; }
}

@keyframes ml-onair {
  0% { box-shadow: 0 0 0 0 var(--live-line); }
  70% { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.ml-chip {
  font-size: 13px; font-weight: 500;
  display: inline-flex; align-items: center; gap: 7px;
  height: 32px; padding: 0 13px; border-radius: var(--r-sm);
  background: transparent; color: var(--fg-2); border: 1px solid var(--line-2);
  cursor: pointer; transition: background 0.14s, color 0.14s, border-color 0.14s;
}

.ml-chip:hover { background: var(--fill-1); color: var(--fg-1); }

.ml-chip[aria-pressed='true'], .ml-chip.is-active { background: var(--violet-tint); color: var(--violet-hi); border-color: var(--violet-line); }

/* ---------- App shell: topbar ---------- */

.ml-topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; gap: 22px;
  height: 58px; padding: 0 22px;
  background: var(--topbar-bg);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.ml-logo { display: flex; align-items: center; gap: 10px; font-family: var(--font-display); font-weight: 700; font-size: 16px; letter-spacing: -0.01em; }

.ml-logo > .ml-brand-mark { width: 28px; height: 28px; flex: none; display: block; }

.ml-logo .mark { width: 28px; height: 28px; border-radius: var(--r-sm); background: linear-gradient(145deg, var(--violet-hi), var(--violet)); color: #f6f4f1; display: grid; place-items: center; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3); }

.ml-logo .mark svg { width: 17px; height: 17px; display: block; }

.ml-logo .sub { font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-2); white-space: nowrap; }

.ml-topbar-right { display: flex; align-items: center; gap: 6px; margin-left: auto; }

.ml-user { position: relative; }

.ml-user-btn { display: flex; align-items: center; gap: 9px; height: 38px; padding: 0 10px 0 6px; border-radius: var(--r-full); background: transparent; border: 1px solid transparent; cursor: pointer; color: var(--fg-1); transition: background 0.14s, border-color 0.14s; }

.ml-user-btn:hover { background: var(--fill-1); border-color: var(--line-2); }

.ml-user-ava { width: 28px; height: 28px; border-radius: var(--r-full); flex: none; background: linear-gradient(145deg, var(--violet-hi), var(--violet)); color: var(--violet-ink, #fff); display: grid; place-items: center; font-family: var(--font-display); font-weight: 600; font-size: 13px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3); }

.ml-user-name { font-size: 13.5px; font-weight: 600; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ml-user-chev { width: 15px; height: 15px; color: var(--fg-3); flex: none; transition: transform 0.16s; }

/* ---------- App shell: sidebar ---------- */

.ml-shell { display: grid; grid-template-columns: 252px 1fr; min-height: calc(100vh - 58px); }

.ml-side { background: var(--bg-raised); border-right: 1px solid var(--line-2); padding: 16px 14px; position: sticky; top: 58px; height: calc(100vh - 58px); overflow-y: auto; }

.ml-side-nav { display: flex; flex-direction: column; gap: 2px; margin-top: 14px; }

.ml-nav-group { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-2); padding: 16px 9px 7px; }

.ml-nav-item { display: flex; align-items: center; gap: 11px; padding: 8px 9px; border-radius: var(--r-sm); color: var(--fg-2); font-size: 13.5px; font-weight: 500; transition: background 0.13s, color 0.13s; cursor: pointer; text-decoration: none; }

.ml-nav-item svg { width: 17px; height: 17px; color: var(--fg-3); transition: color 0.13s; flex: none; }

.ml-nav-item:hover { background: var(--fill-1); color: var(--fg-1); }

.ml-nav-item:hover svg { color: var(--fg-2); }

.ml-nav-item.active { background: var(--violet-tint); color: var(--violet-hi); }

.ml-nav-item.active svg { color: var(--violet-hi); }

.ml-main { padding: 26px 30px 60px; max-width: 1080px; }

@media (max-width: 560px) {
  .ml-topbar { gap: 12px; padding: 0 14px; }
  .ml-logo .sub { display: none; }
  .ml-user-name { max-width: 84px; }
}

/* Phone: the avatar is the account, the name beside it is a second copy of it. */

@media (max-width: 480px) {
  .ml-user-name, .ml-user-chev { display: none; }
  .ml-user-btn { padding: 0 6px; gap: 0; }
}

/* service switcher */

.ml-sw { position: relative; }

.ml-sw-trigger { width: 100%; display: flex; align-items: center; gap: 11px; padding: 9px 11px; border-radius: var(--r); background: var(--bg-inset); border: 1px solid var(--line-2); cursor: pointer; transition: border-color 0.14s, background 0.14s; text-align: left; }

.ml-sw-trigger:hover { border-color: var(--violet-line); }

.ml-sw-ic { width: 34px; height: 34px; border-radius: var(--r); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; flex: none; }

.ml-sw-ic svg { width: 17px; height: 17px; }

.ml-sw-cur { display: flex; flex-direction: column; min-width: 0; flex: 1; }

.ml-sw-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-2); }

.ml-sw-cur-name { font-family: var(--font-display); font-size: 14px; font-weight: 600; color: var(--fg-1); }

.ml-sw-chev { width: 15px; height: 15px; color: var(--fg-3); flex: none; }

/* service switcher popup */

.ml-sw-pop { position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 60; background: var(--bg-raised); border: 1px solid var(--line-2); border-radius: var(--r); box-shadow: var(--shadow-lg); padding: 7px; }

.ml-sw-pop-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-3); padding: 7px 9px 6px; }

.ml-sw-item { display: flex; align-items: center; gap: 10px; padding: 8px 9px; border-radius: var(--r-sm); color: var(--fg-2); font-size: 13.5px; font-weight: 500; transition: background 0.12s, color 0.12s; text-decoration: none; cursor: pointer; }

.ml-sw-item .ml-sw-ic { width: 28px; height: 28px; border-radius: var(--r-sm); background: var(--fill-1); color: var(--fg-3); }

.ml-sw-item .ml-sw-ic svg { width: 15px; height: 15px; }

.ml-sw-item .ml-dot { margin-left: auto; }

.ml-sw-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ml-sw-item:hover { background: var(--fill-1); color: var(--fg-1); }

.ml-sw-item.is-active { background: var(--violet-tint); color: var(--violet-hi); }

.ml-sw-item.is-active .ml-sw-ic { background: var(--violet); color: var(--violet-ink, #fff); }

.ml-sw-pop-foot { border-top: 1px solid var(--line); margin-top: 5px; padding-top: 5px; }

.ml-sw-pop-foot a { display: flex; align-items: center; gap: 9px; padding: 8px 9px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fg-3); text-decoration: none; transition: background 0.12s, color 0.12s; }

.ml-sw-pop-foot a svg { width: 15px; height: 15px; }

.ml-sw-pop-foot a:hover { background: var(--fill-1); color: var(--violet-hi); }

/* user menu popup */

.ml-user-ava.lg { width: 38px; height: 38px; font-size: 17px; border-radius: var(--r-full); }

.ml-user-pop { position: absolute; top: calc(100% + 8px); right: 0; z-index: 60; min-width: 224px; background: var(--bg-raised); border: 1px solid var(--line-2); border-radius: var(--r); box-shadow: var(--shadow-lg); padding: 7px; }

.ml-user-head { display: flex; align-items: center; gap: 11px; padding: 9px 9px 11px; }

.ml-user-meta { display: flex; flex-direction: column; min-width: 0; }

.ml-user-meta .n { font-family: var(--font-display); font-size: 14px; font-weight: 600; color: var(--fg-1); }

.ml-user-meta .r { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-3); margin-top: 2px; }

.ml-user-rule { height: 1px; background: var(--line); margin: 4px 0; }

.ml-user-item { display: flex; align-items: center; gap: 11px; padding: 9px 9px; border-radius: var(--r-sm); color: var(--fg-2); font-size: 13.5px; font-weight: 500; transition: background 0.12s, color 0.12s; text-decoration: none; cursor: pointer; background: none; border: 0; width: 100%; text-align: left; }

.ml-user-item svg { width: 16px; height: 16px; color: var(--fg-3); flex: none; transition: color 0.12s; }

.ml-user-item:hover { background: var(--fill-1); color: var(--fg-1); }

.ml-user-item:hover svg { color: var(--fg-2); }

.ml-user-item.is-danger { color: var(--danger); }

.ml-user-item.is-danger svg { color: var(--danger); }

.ml-user-item.is-danger:hover { background: var(--danger-tint); color: var(--danger-hi); }

/* ---------- Page head ---------- */

.ml-pghead { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; flex-wrap: wrap; margin-bottom: 22px; }

.ml-pghead h1 { margin: 9px 0 8px; }

.ml-pghead p { margin: 0; max-width: 460px; font-size: 13.5px; color: var(--fg-2); }

.ml-pghead-right { display: flex; align-items: center; gap: 14px; }

.ml-hstat { text-align: right; }

.ml-hstat .l { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-3); }

.ml-hstat .v { font-family: var(--font-display); font-size: 20px; font-weight: 700; margin-top: 2px; }

/* ---------- Section content utilities ---------- */

.ml-cols-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.ml-cols-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

.ml-cols-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }

@media (max-width: 820px) { .ml-cols-3, .ml-cols-4 { grid-template-columns: 1fr 1fr; } }

@media (max-width: 560px) { .ml-cols-2, .ml-cols-3, .ml-cols-4 { grid-template-columns: 1fr; } }

.ml-stat { padding: 16px 18px; }

.ml-stat .l { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-3); }

.ml-stat .v { font-family: var(--font-display); font-size: 34px; font-weight: 600; letter-spacing: -0.02em; margin-top: 8px; line-height: 1.05; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.ml-stat .d { font-size: 12px; margin-top: 6px; color: var(--fg-2); }

.ml-row { display: flex; align-items: center; gap: 13px; padding: 13px 16px; border-bottom: 1px solid var(--line); }

.ml-row:last-child { border-bottom: 0; }

.ml-row:hover { background: var(--fill-faint); }

.ml-row-main { flex: 1; min-width: 0; }

.ml-row-t { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.ml-row-s { font-size: 12px; color: var(--fg-3); margin-top: 2px; font-family: var(--font-mono); }

.ml-setrow { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px 0; border-bottom: 1px solid var(--line); }

.ml-setrow:last-child { border-bottom: 0; }

.ml-setrow .sl { font-size: 14px; font-weight: 500; }

.ml-setrow .sd { font-size: 12.5px; color: var(--fg-2); margin-top: 3px; max-width: 380px; }

.ml-ava { width: 32px; height: 32px; border-radius: var(--r-full); background: var(--avatar-bg); display: grid; place-items: center; font-size: 13px; font-weight: 600; color: var(--fg-2); flex: none; }

.ml-empty { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 48px 24px; }

.ml-empty .ic { width: 52px; height: 52px; border-radius: var(--r-lg); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; margin-bottom: 14px; }

.ml-empty .ic svg { width: 24px; height: 24px; }

.ml-empty h3 { font-family: var(--font-display); font-size: 17px; font-weight: 600; margin: 0; }

.ml-empty p { margin: 6px 0 0; font-size: 13.5px; color: var(--fg-2); max-width: 340px; line-height: 1.6; }

.ml-seclabel { display: flex; align-items: center; gap: 14px; margin: 32px 0 18px; }

.ml-seclabel .ml-rule { flex: 1; }

/* Quote blocks for template previews */

.ml-quote { padding: 12px 14px 12px 16px; border-left: 3px solid var(--violet-line); background: var(--fill-faint); border-radius: 0 8px 8px 0; }

.ml-quote + .ml-quote { margin-top: 8px; }

.ml-quote p { margin: 0; font-size: 14px; line-height: 1.5; color: var(--fg-1); }

.ml-quote .src { font-size: 11px; color: var(--fg-3); margin-top: 4px; font-family: var(--font-mono); letter-spacing: 0.06em; }

.ml-stack > * + * { margin-top: var(--ml-gap, 20px); }

/* ═══════════════════════════════════════════════════════════════════════════
   LIGHT THEME COMPATIBILITY
   Many existing components use Tailwind opacity-white/cyan utilities designed
   for dark backgrounds. These overrides remap them to dark-on-light equivalents
   so all services render correctly on [data-theme="light"].
   ══════════════════════════════════════════════════════════════════════════ */

/* Text: white-opacity → dark text (same relative weight, readable on paper bg) */

[data-theme="light"] [class*="text-white"]    { color: var(--fg-2) !important; }

[data-theme="light"] .text-white              { color: var(--fg-1) !important; }

/* Backgrounds: white-opacity → near-transparent dark surface */

[data-theme="light"] [class*="bg-white/"]     { background-color: rgba(13,11,20,0.04) !important; }

/* Backgrounds: black overlays stay as-is (they add subtle depth on light too) */

/* Borders: white-opacity → hairline dark */

[data-theme="light"] [class*="border-white/"] { border-color: rgba(13,11,20,0.14) !important; }

/* Dividers */

[data-theme="light"] [class*="divide-white"]  > * + * { border-color: rgba(13,11,20,0.10) !important; }

/* The cyan→violet shim lived here: seven `[class*="…cyan"] { … !important }`
   rules that repainted every legacy cyan Tailwind class as the accent, in both
   themes. It did its job — but it hid its own callers (grepping for `cyan` to
   find wrong colours returned 405 hits that were already right), it flattened
   authored weight (`bg-cyan-200/[0.12]` and `/[0.08]` both landed on
   `--violet-tint`, verified identical in the engine), and `[class*="to-cyan"]`
   also caught `to-cyan-500` inside multichat's user-pickable widget gradients —
   so a streamer who chose the sky→cyan preset got sky→violet.

   All 405 call sites now name the token they want, so the shim is gone rather
   than inherited. */

/* ═══════════════════════════════════════════════════════════════════════════
   NOTIFY SERVICE — design-handoff patterns (memelab-notify-mockup)
   Flat Console patterns specific to the Notify screens. Ported verbatim from
   the designer's mockup; tokens already exist in tokens.css.
   ══════════════════════════════════════════════════════════════════════════ */

/* Narrow content column for single-column sections (Шаблон/Анонс/Площадки) */

.nfy-narrow { max-width: 820px; }

/* REMOVED: the overview feature-card set (`.features` / `.feat` / `.fic`).
   It shipped "three equal cards with a violet icon tile on top" as a
   first-class component of the design system, which is the single most
   named symptom on the charter's anti-pattern list — so every service
   that reached for the system reached for the tell. Its one consumer was
   the notify overview, which now answers whether announcements actually
   went out instead of listing what the service can do. If a screen needs
   to explain a product to somebody who has already signed up for it, the
   screen is the problem, not the missing class. */

/* Provider tile (telegram / discord — the only color cue we allow) */

.nfy-prov { display:grid; place-items:center; flex:none; border:1px solid; width:40px; height:40px; border-radius: var(--r); }

.nfy-prov svg { width:19px; height:19px; }

.nfy-prov.tg { background: rgba(47,155,212,.12); color:#2F9BD4; border-color: rgba(47,155,212,.26); }

.nfy-prov.dc { background: rgba(97,104,232,.12); color:#6168E8; border-color: rgba(97,104,232,.26); }

[data-theme="light"] .nfy-prov.tg { color:#1B83BC; }

[data-theme="light"] .nfy-prov.dc { color:#4750D8; }

/* Telegram link line — quiet, single row */

.nfy-tglink { display:flex; align-items:center; gap:13px; padding:14px 18px; }

.nfy-tglink .ic { color: var(--live); display:flex; flex:none; }

.nfy-tglink .ic svg { width:18px; height:18px; }

.nfy-tglink .tt { flex:1; font-size:13.5px; color:var(--fg-2); }

.nfy-tglink .tt b { color: var(--fg); font-weight:600; }

.nfy-tglink .tt a { color: var(--violet-hi); font-family: var(--font-mono); font-size:12.5px; }

/* Channel card — flat, no nested boxes */

.nfy-chan { padding:16px 18px; }

.nfy-chan + .nfy-chan { border-top:1px solid var(--line); }

.nfy-chan-head { display:flex; align-items:center; gap:13px; }

.nfy-chan-head .m { flex:1; min-width:0; }

.nfy-chan-title { display:flex; align-items:center; gap:9px; flex-wrap:wrap; }

.nfy-chan-title h3 { font-family: var(--font-display); font-size:14.5px; font-weight:600; margin:0; }

.nfy-chan-id { font-family: var(--font-mono); font-size:12px; color:var(--fg-3); margin-top:3px; }

.nfy-chan-foot { display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-top:13px; padding-top:13px; border-top:1px solid var(--line); }

.nfy-mini-toggle { display:flex; align-items:center; gap:10px; font-size:13px; color:var(--fg-2); }

.nfy-chan-actions { display:flex; align-items:center; gap:4px; }

.nfy-chan-last { display:flex; align-items:center; gap:7px; margin-top:11px; font-size:11.5px; color:var(--fg-3); font-family:var(--font-mono); }

.nfy-chan-last svg { width:13px; height:13px; }

/* Collapsible disclosure */

.nfy-collapse { margin-top:13px; }

.nfy-collapse-trigger { display:inline-flex; align-items:center; gap:7px; background:none; border:0; cursor:pointer; color:var(--fg-2); font-family:var(--font-body); font-size:12.5px; font-weight:500; padding:0; }

.nfy-collapse-trigger:hover { color:var(--violet-hi); }

.nfy-collapse-trigger .chev { transition: transform .16s; width:14px; height:14px; color:var(--fg-3); flex:none; }

.nfy-collapse.open .nfy-collapse-trigger { color:var(--fg); }

.nfy-collapse.open .nfy-collapse-trigger .chev { transform: rotate(90deg); }

.nfy-collapse-body { display:none; margin-top:12px; }

.nfy-collapse.open .nfy-collapse-body { display:block; }

/* Segmented control */

.nfy-seg { display:inline-flex; padding:3px; gap:2px; background:var(--bg-inset); border:1px solid var(--line-2); border-radius:var(--r-sm); }

.nfy-seg button { font-family:var(--font-body); font-size:13px; font-weight:600; padding:6px 15px; border-radius: var(--r-xs); border:0; background:none; color:var(--fg-2); cursor:pointer; transition:background .14s,color .14s; white-space:nowrap; }

.nfy-seg button:hover { color:var(--fg); }

.nfy-seg button.is-active { background:var(--violet); color:var(--violet-ink, #fff); }

/* Photo radio — compact cards */

.nfy-radio-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }

.nfy-radio, button.nfy-plat { color: var(--fg); }

.nfy-radio { position:relative; text-align:left; padding:14px 15px; border-radius:var(--r); border:1px solid var(--line-2); background:var(--bg-raised); cursor:pointer; transition:border-color .14s, background .14s; display:block; width:100%; }

.nfy-radio:hover { border-color: var(--violet-line); }

.nfy-radio.is-active { border-color: var(--violet-line); background: var(--violet-tint); }

.nfy-radio .rt { font-weight:600; font-size:13.5px; display:flex; align-items:center; gap:8px; }

.nfy-radio .rt svg { width:16px; height:16px; color:var(--fg-2); }

.nfy-radio.is-active .rt svg { color:var(--violet-hi); }

.nfy-radio .rd { font-size:12px; color:var(--fg-2); margin-top:5px; }

.nfy-radio .rmark { position:absolute; top:12px; right:12px; width:8px; height:8px; border-radius: var(--r-full); background:var(--violet); opacity:0; transition:opacity .14s; }

.nfy-radio.is-active .rmark { opacity:1; }

/* Template variable chips */

.nfy-vars { display:flex; flex-wrap:wrap; gap:7px; }

.nfy-vchip { font-family:var(--font-mono); font-size:12px; padding:5px 9px; border-radius: var(--r-xs); border:1px solid var(--line); background:var(--fill-faint); color:var(--violet-hi); cursor:pointer; transition:border-color .14s, background .14s; }

.nfy-vchip:hover { border-color:var(--violet-line); background:var(--violet-tint); }

/* Telegram inline-button preview */

.nfy-tgbtns { display:flex; flex-wrap:wrap; gap:8px; }

.nfy-tgbtn { display:inline-flex; align-items:center; gap:7px; height:32px; padding:0 13px; border-radius:var(--r-sm); background: var(--fill-1); border:1px solid var(--line-2); font-size:13px; font-weight:500; color:var(--fg); }

/* Custom buttons editor rows */

.nfy-btnrow { display:grid; grid-template-columns: minmax(0,1fr) minmax(0,1.4fr) auto; gap:9px; align-items:center; }

.nfy-btnrow + .nfy-btnrow { margin-top:9px; }

/* Info note */

.nfy-note { display:flex; gap:12px; padding:14px 16px; border-radius:var(--r); border:1px solid var(--line); background:var(--fill-faint); }

.nfy-note .ic { color:var(--violet-hi); flex:none; }

.nfy-note .ic svg { width:17px; height:17px; }

.nfy-note p { margin:0; font-size:13px; color:var(--fg-2); line-height:1.6; }

.nfy-note a { color:var(--violet-hi); font-weight:600; }

/* Instruction steps */

.nfy-steps { display:flex; flex-direction:column; }

.nfy-step { display:flex; gap:12px; padding:10px 0; border-bottom:1px solid var(--line); font-size:13px; color:var(--fg-2); line-height:1.5; }

.nfy-step:last-child { border-bottom:0; }

.nfy-step .n { width:22px; height:22px; border-radius: var(--r-sm); background:var(--violet-tint); color:var(--violet-hi); font-family:var(--font-mono); font-size:11px; font-weight:600; display:grid; place-items:center; flex:none; }

.nfy-step b { color:var(--fg); font-weight:600; }

.nfy-step code { font-family:var(--font-mono); font-size:.92em; background:var(--fill-1); color:var(--violet-hi); padding:1px 6px; border-radius: var(--r-xs); }

/* Platform row */

.nfy-plat { display:flex; align-items:center; gap:13px; padding:13px 16px; border-bottom:1px solid var(--line); }

.nfy-plat:last-child { border-bottom:0; }

.nfy-plat .pic { width:34px; height:34px; border-radius: var(--r); display:grid; place-items:center; flex:none; background: var(--fill-1); color: var(--fg-2); border:1px solid var(--line); }

.nfy-plat .pic svg { width:17px; height:17px; }

.nfy-plat .pmain { flex:1; min-width:0; }

.nfy-plat .pn { font-size:13.5px; font-weight:500; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }

.nfy-plat .pu { font-family:var(--font-mono); font-size:12px; color:var(--fg-3); margin-top:2px; }

.nfy-counter { font-family:var(--font-mono); font-size:11px; color:var(--fg-3); }

/* Row helper */

.nfy-row-between { display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; }

/* Modal */

.nfy-scrim { position:fixed; inset:0; background:rgba(0,0,0,.6); backdrop-filter:blur(3px); display:grid; place-items:center; z-index:200; padding:20px; }

[data-theme="light"] .nfy-scrim { background:rgba(40,32,22,.42); }

.nfy-modal { width:100%; max-width:480px; background:var(--bg-raised); border:1px solid var(--line-2); border-radius:var(--r-lg); box-shadow:var(--shadow-lg); overflow:hidden; }

.nfy-modal-head { display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid var(--line); }

.nfy-modal-head h3 { font-family:var(--font-display); font-size:16px; font-weight:600; margin:0; }

.nfy-modal-body { padding:20px; }

.nfy-modal-foot { display:flex; justify-content:flex-end; gap:10px; padding:14px 20px; border-top:1px solid var(--line); }

/* Mobile — collapse multi-column grids */

@media (max-width: 720px) {
  .nfy-radio-grid { grid-template-columns: 1fr; }
  .nfy-btnrow { grid-template-columns: 1fr auto; }
  .nfy-btnrow .url { grid-column: 1 / -1; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CHATBOT SERVICE — design-handoff patterns (chatbot.html)
   Flat Console patterns for the Chatbot screens. Ported from the designer
   mockup; tokens already exist in tokens.css → both themes work.
   ══════════════════════════════════════════════════════════════════════════ */

/* Nav item: label fills row, badge dot pinned right */

.ml-nav-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ml-nav-badge { width: 7px; height: 7px; border-radius: var(--r-full); background: var(--warn); flex: none; }

@media (prefers-reduced-motion: no-preference) {
  .ml-nav-badge { animation: ml-onair 2.4s ease-in-out infinite; }
}

/* Alert / reauth banner */

.ml-banner { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 16px 18px; border-radius: var(--r); border: 1px solid var(--line); margin-bottom: 18px; }

.ml-banner--warn { background: var(--warn-tint); border-color: var(--warn-line); }

.ml-banner--danger { background: var(--danger-tint); border-color: var(--danger-line); }

.ml-banner-body { flex: 1; min-width: 260px; }

.ml-banner-body strong { display: block; font-family: var(--font-display); font-size: 14.5px; font-weight: 600; color: var(--fg-1); margin-bottom: 5px; }

.ml-banner-body p { margin: 0; font-size: 13px; color: var(--fg-2); line-height: 1.55; }

.ml-banner-list { margin: 8px 0 0; padding-left: 18px; font-size: 13px; color: var(--fg-2); }

.ml-banner-list li { margin: 2px 0; }

/* Overview dashboard grids */

.cb-grid-main { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(280px,1fr); gap: 16px; }

.cb-grid-sub  { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.15fr); gap: 16px; margin-top: 16px; }

@media (max-width: 860px) { .cb-grid-main, .cb-grid-sub { grid-template-columns: 1fr; } }

/* Live chat console */

.cb-console { padding: 12px; }

.cb-bot-tag { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .06em; background: var(--violet-tint); color: var(--violet-hi); padding: 1px 6px; border-radius: var(--r-xs); margin-right: 6px; }

.cb-trig-tag { font-family: var(--font-mono); font-size: 11px; background: var(--fill-1); color: var(--fg-3); padding: 1px 6px; border-radius: var(--r-xs); margin: 0 6px; }

/* Resource gauge */

.cb-res-row { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 10px; }

.cb-res-row .big { font-family: var(--font-display); font-size: 26px; font-weight: 700; }

.cb-res-row .lim { font-family: var(--font-mono); font-size: 12px; color: var(--fg-3); }

.cb-bar { height: 7px; border-radius: var(--r-full); background: var(--fill-2); overflow: hidden; }

.cb-bar > span { display: block; height: 100%; border-radius: var(--r-full); background: var(--violet); }

.cb-bar.warn > span { background: var(--warn); }

.cb-bar.danger > span { background: var(--danger); }

/* Module toggle list */

.cb-mod { display: flex; align-items: center; justify-content: space-between; padding: 11px 13px; border-radius: var(--r-sm); border: 1px solid var(--line); transition: border-color .13s, background .13s; cursor: pointer; text-decoration: none; }

.cb-mod + .cb-mod { margin-top: 8px; }

.cb-mod:hover { border-color: var(--line-2); background: var(--fill-faint); }

.cb-mod .lab { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--fg-1); }

.cb-mod .lab svg { width: 16px; height: 16px; color: var(--fg-3); }

.cb-mod.on .lab svg { color: var(--violet-hi); }

/* Answer routes grid */

.cb-routes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 16px; }

.cb-route { border: 1px solid var(--line); border-radius: var(--r-sm); padding: 13px; }

.cb-route .rl { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }

.cb-route .rl span { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); }

.cb-route .rv { font-family: var(--font-display); font-size: 17px; font-weight: 600; }

/* Metric footer (two cells, divided) */

.cb-metric-foot { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--line); }

.cb-metric-foot > div { padding: 13px 16px; }

.cb-metric-foot > div + div { border-left: 1px solid var(--line); }

.cb-metric-foot .v { font-family: var(--font-display); font-size: 22px; font-weight: 700; }

.cb-metric-foot .l { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); margin-top: 2px; }

/* Setup step row (onboarding-style icon + label) */

.cb-step-ic { width: 26px; height: 26px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; }

.cb-step-ic.done { background: var(--live-tint); color: var(--live-hi); }

.cb-step-ic.todo { background: var(--violet); color: var(--violet-ink, #fff); font-family: var(--font-mono); font-size: 12px; font-weight: 600; }

.cb-step-ic svg { width: 14px; height: 14px; }

/* ──────────────────────────────────────────────────────────────────────────
   CHATBOT — finished mockup component patterns (chatbot.html v17)
   All on tokens → dark + light. cb- namespaced to avoid Tailwind collisions.
   ────────────────────────────────────────────────────────────────────────── */

/* Page layout helpers */

.cb-narrow { max-width: 880px; }

.cb-grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.1fr); gap: 16px; }

@media (max-width: 820px) { .cb-grid-2 { grid-template-columns: 1fr; } }

.cb-kpis { display: grid; grid-template-columns: repeat(3,1fr); gap: 14px; margin-bottom: 16px; }

@media (max-width: 680px) { .cb-kpis { grid-template-columns: 1fr; } }

.cb-tiles { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; }

@media (max-width: 820px) { .cb-tiles { grid-template-columns: 1fr 1fr; } }

/* Two-column CRM (форма + список): Заметки / Цитаты / FAQ */

.cb-crm { display: grid; grid-template-columns: minmax(0,360px) minmax(0,1fr); gap: 16px; }

@media (max-width: 820px) { .cb-crm { grid-template-columns: 1fr; } }

.cb-field + .cb-field { margin-top: 14px; }

/* Status board (merged Модули + Маршруты) */

.cb-board { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; padding: 14px; }

@media (max-width: 520px) { .cb-board { grid-template-columns: 1fr; } }

.cb-board .cell { display: flex; align-items: center; justify-content: space-between; padding: 11px 13px; border: 1px solid var(--line); border-radius: var(--r-sm); }

.cb-board .cell .lab { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--fg-1); }

.cb-board .cell .lab svg { width: 16px; height: 16px; color: var(--fg-3); }

.cb-board .cell.on .lab svg { color: var(--violet-hi); }

/* Preset cards (single-select) */

.cb-presets { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; }

@media (max-width: 720px) { .cb-presets { grid-template-columns: 1fr 1fr; } }

.cb-preset { text-align: left; padding: 13px; border: 1px solid var(--line-2); border-radius: var(--r); background: var(--bg-raised); color: var(--fg-1); font-family: var(--font-body); cursor: pointer; transition: border-color .13s, background .13s; }

.cb-preset:hover { border-color: var(--violet-line); }

.cb-preset.is-active { border-color: var(--violet-line); background: var(--violet-tint); }

.cb-preset .pt { font-weight: 600; font-size: 13.5px; color: var(--fg-1); }

.cb-preset.is-active .pt { color: var(--violet-hi); }

.cb-preset .pd { font-size: 11.5px; color: var(--fg-2); margin-top: 5px; line-height: 1.4; }

/* Segmented control */

.cb-seg { display: inline-flex; padding: 3px; gap: 2px; background: var(--bg-inset); border: 1px solid var(--line-2); border-radius: var(--r-sm); }

.cb-seg button { font-family: var(--font-body); font-size: 13px; font-weight: 600; padding: 6px 15px; border-radius: var(--r-xs); border: 0; background: none; color: var(--fg-2); cursor: pointer; transition: background .13s, color .13s; }

.cb-seg button.is-active { background: var(--violet); color: var(--violet-ink, #fff); }

/* Expandable trigger card (Реакции) */

.cb-trig { border-top: 1px solid var(--line); }

.cb-trig:first-child { border-top: 0; }

.cb-trig-head { display: flex; align-items: center; gap: 11px; padding: 13px 16px; }

.cb-trig-head .ic { width: 34px; height: 34px; border-radius: var(--r); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; flex: none; }

.cb-trig-head .ic svg { width: 17px; height: 17px; }

.cb-trig.is-off .cb-trig-head .ic { background: var(--fill-1); color: var(--fg-3); }

.cb-trig-head .m { flex: 1; min-width: 0; }

.cb-trig-head .t { font-weight: 600; font-size: 14px; }

.cb-trig-head .s { font-size: 12px; color: var(--fg-3); font-family: var(--font-mono); margin-top: 2px; }

.cb-trig-chev { width: 18px; height: 18px; color: var(--fg-3); transition: transform .18s; flex: none; }

.cb-trig.is-open .cb-trig-chev { transform: rotate(180deg); }

.cb-trig-body { padding: 0 16px 16px 61px; }

.cb-trig-body .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

@media (max-width: 620px) { .cb-trig-body .row2 { grid-template-columns: 1fr; } .cb-trig-body { padding-left: 16px; } }

/* Reward cards */

.cb-reward-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 12px; padding: 16px; }

@media (max-width: 720px) { .cb-reward-grid { grid-template-columns: 1fr; } }

.cb-reward { border: 1px solid var(--line); border-radius: var(--r); padding: 14px; }

.cb-reward .ic { width: 36px; height: 36px; border-radius: var(--r); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; margin-bottom: 10px; }

.cb-reward.is-off .ic { background: var(--fill-1); color: var(--fg-3); }

/* Flat channel card */

.cb-chan { border-top: 1px solid var(--line); }

.cb-chan:first-child { border-top: 0; }

.cb-chan-head { display: flex; align-items: center; gap: 13px; padding: 14px 16px; }

.cb-chan-ic { width: 38px; height: 38px; border-radius: var(--r); background: var(--fill-1); color: var(--fg-3); display: grid; place-items: center; flex: none; }

.cb-chan-ic svg { width: 19px; height: 19px; }

.cb-chan.on .cb-chan-ic { color: var(--violet-hi); }

/* The channel name is the SUBJECT of the row; the platform is its category.
   `.t` (platform) used to be set 16px semibold and `.s` (channel) 12px mono, so
   a list of one streamer's channels read as «Twitch / Twitch / YouTube» three
   times over, and `CoolStreamer YT` vs `CoolStreamer Live` — the only thing
   that actually differs between the rows — could not be told apart at a glance.
   Category small and quiet, subject large and bright. */

.cb-chan-m { flex: 1; min-width: 0; }

.cb-chan-m .t {
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.cb-chan-m .s { font-size: 15px; font-weight: 500; color: var(--fg-1); margin-top: 5px; }

.cb-chan.off .cb-chan-m .s { color: var(--fg-2); }

.cb-chan-check-btn { width: 30px; height: 30px; border-radius: var(--r-sm); background: transparent; border: 0; color: var(--fg-3); display: grid; place-items: center; cursor: pointer; transition: background .13s, color .13s; }

.cb-chan-check-btn:hover { background: var(--fill-1); color: var(--fg-1); }

.cb-chan-check-btn svg { width: 17px; height: 17px; transition: transform .18s; }

.cb-chan.is-open .cb-chan-check-btn svg { transform: rotate(180deg); }

.cb-chan-check { padding: 2px 16px 16px 67px; }

@media (max-width: 620px) { .cb-chan-check { padding-left: 16px; } }

.cb-chk { display: flex; align-items: center; gap: 9px; padding: 7px 0; font-size: 13px; color: var(--fg-2); }

.cb-chk svg { width: 15px; height: 15px; flex: none; }

.cb-chk.ok svg { color: var(--live); }

.cb-chk.no svg { color: var(--fg-3); }

/* Analytics: top sources */

.cb-src-row { display: flex; align-items: center; gap: 13px; padding: 13px 16px; border-bottom: 1px solid var(--line); }

.cb-src-row:last-child { border-bottom: 0; }

.cb-src-row .nm { width: 140px; font-size: 13.5px; flex: none; }

.cb-src-row .tr { flex: 1; }

.cb-src-row .vl { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-2); width: 64px; text-align: right; flex: none; }

/* Decision log rows (verb + reason) */

.cb-dec { display: flex; align-items: center; gap: 13px; padding: 13px 16px; border-bottom: 1px solid var(--line); }

.cb-dec:last-child { border-bottom: 0; }

.cb-dec .v { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .06em; padding: 3px 8px; border-radius: var(--r-xs); flex: none; }

.cb-dec .v.yes { background: var(--live-tint); color: var(--live-hi); }

.cb-dec .v.no  { background: var(--fill-1); color: var(--fg-3); }

.cb-dec .v.blk { background: var(--warn-tint); color: var(--warn-hi); }

.cb-dec .m { flex: 1; min-width: 0; }

.cb-dec .m .t { font-size: 13.5px; font-weight: 500; }

.cb-dec .m .s { font-size: 12px; color: var(--fg-3); margin-top: 2px; }

.cb-dec .tm { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); flex: none; }

/* Recap accordion */

.cb-recap-head { display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px; cursor: pointer; transition: background .13s; }

.cb-recap-head:hover { background: var(--fill-faint); }

.cb-recap-head .sum { flex: 1; min-width: 0; }

.cb-recap-head .sum .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }

.cb-recap-chev { width: 18px; height: 18px; color: var(--fg-3); transition: transform .18s; flex: none; margin-top: 2px; }

.cb-recap.is-open .cb-recap-chev { transform: rotate(180deg); }

.cb-recap-body { border-top: 1px solid var(--line); padding: 16px 18px; display: grid; grid-template-columns: repeat(3,1fr); gap: 14px; }

@media (max-width: 720px) { .cb-recap-body { grid-template-columns: 1fr; } }

.cb-recap-col h4 { font-family: var(--font-display); font-size: 13px; font-weight: 600; margin: 0 0 9px; }

.cb-recap-col .it { font-size: 12.5px; color: var(--fg-2); line-height: 1.5; padding: 7px 0; border-top: 1px solid var(--line); }

.cb-recap-col .it:first-of-type { border-top: 0; }

/* Emotes grid */

.cb-emote-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(98px,1fr)); gap: 10px; padding: 16px; }

.cb-emote { position: relative; border: 1px solid var(--line); border-radius: var(--r); padding: 12px 8px 10px; text-align: center; cursor: pointer; background: none; font-family: var(--font-body); transition: border-color .13s, background .13s; }

.cb-emote:hover { border-color: var(--line-2); background: var(--fill-faint); }

.cb-emote.on { border-color: var(--violet-line); background: var(--violet-tint); }

.cb-emote .thumb { width: 46px; height: 46px; border-radius: var(--r); margin: 0 auto 8px; background: var(--fill-1); display: grid; place-items: center; color: var(--fg-3); font-family: var(--font-mono); font-size: 13px; font-weight: 600; overflow: hidden; }

.cb-emote .thumb img { width: 100%; height: 100%; -o-object-fit: contain; object-fit: contain; }

.cb-emote.on .thumb { color: var(--violet-hi); }

.cb-emote:not(.on) .thumb { opacity: .6; }

.cb-emote .en { font-size: 11.5px; color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.cb-emote.on .en { color: var(--fg-1); }

.cb-emote .ob { position: absolute; top: 7px; right: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .05em; color: var(--fg-3); }

.cb-emote.on .ob { display: none; }

/* Moderation rule rows */

.cb-rule { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--line); }

.cb-rule:last-child { border-bottom: 0; }

.cb-rule .rm { flex: 1; min-width: 0; }

.cb-rule .rt { font-size: 14px; font-weight: 500; }

.cb-rule .rd { font-size: 12.5px; color: var(--fg-2); margin-top: 3px; max-width: 460px; }

.cb-rule .ml-select { width: 120px; height: 32px; font-size: 12.5px; flex: none; padding-right: 30px; }

.cb-rule.is-off { opacity: .5; }

/* Chip-input list (ignored bots / blacklist) */

.cb-taglist { display: flex; flex-wrap: wrap; gap: 8px; }

.cb-tag { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 5px 0 11px; border-radius: var(--r-sm); background: var(--fill-1); border: 1px solid var(--line-2); font-family: var(--font-mono); font-size: 12px; color: var(--fg-1); }

.cb-tag button { position: relative; width: 18px; height: 18px; border: 0; background: none; color: var(--fg-3); cursor: pointer; display: grid; place-items: center; border-radius: var(--r-xs); }

/* 18×18 is well under the 40px floor. A full 40px skirt cannot fit — the tags
   sit 8px apart and it would swallow the neighbour's remove button — so this
   takes the tag's whole height and 3px of side, 24×30, and leaves 2px between
   two adjacent skirts. Better, and deliberately not the floor. */

.cb-tag button::after { content: ''; position: absolute; inset: -6px -3px; }

.cb-tag button:hover { background: var(--fill-2); color: var(--fg-1); }

.cb-tag button svg { width: 13px; height: 13px; }

/* Range slider + endpoints */

.cb-range { width: 100%; accent-color: var(--violet); }

.cb-sl-ends { display: flex; justify-content: space-between; margin-top: 5px; font-size: 11px; color: var(--fg-3); }

/* Personality builder generated-text card */

.cb-modal-gen { border: 1px solid var(--violet-line); background: var(--violet-tint); border-radius: var(--r); padding: 14px 16px; }

.cb-modal-gen .gl { font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--violet-hi); margin-bottom: 8px; }

.cb-modal-gen p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--fg-1); }

/* ──────────────────────────────────────────────────────────────────────────
   CHATBOT — mockup v2 additions (онбординг, админ, обогащённые экраны)
   ────────────────────────────────────────────────────────────────────────── */

/* TTS engine preset: "soon" placeholder badge */

.cb-preset .pt-row { display: flex; align-items: center; gap: 8px; }

.cb-tts-badge { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; padding: 2px 6px; border-radius: var(--r-xs); background: var(--warn-tint); color: var(--warn-hi); border: 1px solid var(--warn-line); }

/* Disclosure (Дополнительно / эксперт) */

.cb-disc { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }

.cb-disc > summary { display: flex; align-items: center; gap: 10px; padding: 14px 16px; cursor: pointer; list-style: none; font-size: 13.5px; font-weight: 600; color: var(--fg-2); }

.cb-disc > summary::-webkit-details-marker { display: none; }

.cb-disc > summary .dchev { width: 17px; height: 17px; color: var(--fg-3); margin-left: auto; transition: transform .18s; }

.cb-disc[open] > summary .dchev { transform: rotate(180deg); }

.cb-disc > summary .dl { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); font-weight: 600; }

.cb-disc-body { padding: 2px 16px 16px; }

/* Status board: "setup needed" / "on" states */

.cb-board .cell.setup { border-color: var(--warn-line); }

.cb-board .cell.setup .lab svg { color: var(--warn); }

.cb-board .cell .st { font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; color: var(--fg-3); }

.cb-board .cell.setup .st { color: var(--warn); }

.cb-board .cell.on .st { color: var(--live); }

.cb-board .cell.down { border-color: var(--danger-line); }

.cb-board .cell.down .lab svg { color: var(--danger); }

.cb-board .cell.down .st { color: var(--danger); }

/* Trigger: category group header + badges */

.cb-trig-group { display: flex; align-items: center; gap: 10px; padding: 13px 16px 9px; }

.cb-trig-group .gt { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); font-weight: 600; }

.cb-trig-group .gc { font-family: var(--font-mono); font-size: 11px; color: var(--violet-hi); }

.cb-trig-group .gl { flex: 1; height: 1px; background: var(--line); }

.cb-tbadges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.cb-tbadge { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .03em; padding: 2px 7px; border-radius: var(--r-xs); background: var(--fill-1); color: var(--fg-3); }

.cb-tbadge.v { background: var(--violet-tint); color: var(--violet-hi); }

.cb-tbadge.ov { background: var(--warn-tint); color: var(--warn-hi); }

.cb-trig-head .mini { display: flex; align-items: center; gap: 6px; }

.cb-trig-chev-btn { background: transparent; border: 0; cursor: pointer; display: grid; place-items: center; padding: 2px; color: var(--fg-3); }

/* Full reward card (source + edit/delete + link status) */

.cb-rwd { border: 1px solid var(--line); border-radius: var(--r); padding: 14px; display: flex; flex-direction: column; gap: 11px; }

.cb-rwd-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 12px; padding: 16px; }

@media (max-width: 720px) { .cb-rwd-grid { grid-template-columns: 1fr; } }

.cb-rwd-top { display: flex; align-items: flex-start; gap: 11px; }

.cb-rwd-ic { width: 38px; height: 38px; border-radius: var(--r); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; flex: none; }

.cb-rwd-ic svg { width: 19px; height: 19px; }

.cb-rwd.is-unlinked .cb-rwd-ic { background: var(--fill-1); color: var(--fg-3); }

.cb-rwd-m { flex: 1; min-width: 0; }

.cb-rwd-m .t { font-weight: 600; font-size: 14px; }

.cb-rwd-m .c { font-family: var(--font-mono); font-size: 12px; color: var(--fg-3); margin-top: 2px; }

.cb-rwd-src { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: .04em; padding: 2px 7px; border-radius: var(--r-xs); background: var(--fill-1); color: var(--fg-3); white-space: nowrap; flex: none; }

.cb-rwd-link { display: flex; align-items: center; gap: 8px; font-size: 12.5px; padding: 9px 11px; border-radius: var(--r-sm); background: var(--fill-faint); }

.cb-rwd-link .lk-t { flex: 1; min-width: 0; color: var(--fg-2); }

.cb-rwd-link .lk-t b { color: var(--fg-1); font-weight: 600; }

.cb-rwd-acts { display: flex; gap: 8px; align-items: center; }

/* Viewer memory cards (filled state) */

.cb-vc-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(220px,1fr)); gap: 12px; padding: 16px; }

.cb-vcard { text-align: left; border: 1px solid var(--line); border-radius: var(--r); padding: 13px; background: var(--bg-raised); cursor: pointer; font-family: var(--font-body); transition: border-color .13s, background .13s; }

.cb-vcard:hover { border-color: var(--violet-line); }

.cb-vcard.is-active { border-color: var(--violet-line); background: var(--violet-tint); }

.cb-vcard-top { display: flex; align-items: center; gap: 10px; }

.cb-vcard .va { width: 32px; height: 32px; border-radius: var(--r); background: var(--fill-1); color: var(--fg-2); display: grid; place-items: center; font-weight: 600; font-size: 13px; flex: none; }

.cb-vcard.is-active .va { background: var(--violet); color: var(--violet-ink, #fff); }

.cb-vcard .vn { font-weight: 600; font-size: 13.5px; }

.cb-vcard .vmeta { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); margin-top: 1px; }

.cb-vcard .vtags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }

.cb-vcard .vtag { font-size: 11px; padding: 2px 8px; border-radius: var(--r-full); background: var(--fill-1); color: var(--fg-2); }

.cb-vc-detail .dl-fact { display: flex; gap: 10px; padding: 11px 0; border-top: 1px solid var(--line); font-size: 13px; }

.cb-vc-detail .dl-fact:first-child { border-top: 0; }

.cb-vc-detail .dl-fact .k { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); width: 120px; flex: none; padding-top: 1px; }

.cb-vc-detail .dl-fact .vv { color: var(--fg-2); flex: 1; }

/* Conditional simulation fields */

.cb-sim-cond { display: none; }

.cb-sim-cond.show { display: block; }

/* Onboarding wizard */

.cb-ob-step { display: flex; gap: 14px; padding: 16px; border: 1px solid var(--line); border-radius: var(--r); }

.cb-ob-step + .cb-ob-step { margin-top: 12px; }

.cb-ob-step.done { border-color: var(--live-line); }

.cb-ob-num { width: 30px; height: 30px; border-radius: var(--r-full); background: var(--violet-tint); color: var(--violet-hi); display: grid; place-items: center; font-family: var(--font-mono); font-weight: 600; font-size: 13px; flex: none; }

.cb-ob-step.done .cb-ob-num { background: var(--live-tint); color: var(--live-hi); }

.cb-ob-m { flex: 1; }

.cb-ob-m .obt { font-weight: 600; font-size: 14px; }

.cb-ob-m .obd { font-size: 12.5px; color: var(--fg-2); margin-top: 3px; }

.cb-admin-warn { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--warn); }

/* Decision log: filters + expandable row (профиль/трейс/хеш) */

.cb-log-filters { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

.cb-log-filters .ml-select { width: auto; min-width: 130px; height: 34px; }

.cb-logrow { border-bottom: 1px solid var(--line); }

.cb-logrow:last-child { border-bottom: 0; }

.cb-logrow .cb-dec { border-bottom: 0; cursor: pointer; }

.cb-logrow .cb-dec:hover { background: var(--fill-faint); }

.cb-log-chev { width: 16px; height: 16px; color: var(--fg-3); transition: transform .18s; flex: none; }

.cb-logrow.is-open .cb-log-chev { transform: rotate(180deg); }

.cb-log-body { padding: 2px 16px 16px 16px; }

.cb-log-grid { display: grid; grid-template-columns: 160px 1fr; gap: 9px 14px; padding: 13px 15px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--fill-faint); }

.cb-log-grid .k { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); }

.cb-log-grid .vv { font-size: 13px; color: var(--fg-2); }

.cb-log-grid .vv.mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--fg-3); word-break: break-all; }

.cb-log-trace { display: flex; flex-direction: column; gap: 0; }

.cb-log-trace .ts { display: flex; gap: 9px; align-items: flex-start; font-size: 12.5px; color: var(--fg-2); padding: 3px 0; }

.cb-log-trace .ts .n { font-family: var(--font-mono); font-size: 11px; color: var(--violet-hi); width: 16px; flex: none; padding-top: 2px; }

/* ── Narrow-width overrides live LAST, on purpose ──────────────────────
   This block used to sit up with the shell section, above the component
   rules it overrides and at equal specificity, so three of its
   declarations never applied: `.ml-sw-trigger` kept the desktop width,
   padding and gap; `.ml-sw-cur` stayed visible instead of collapsing the
   switcher to its icon; and `.ml-pghead-right` kept the 14px desktop gap.
   Each was a stated intention doing nothing, and chatbot had already
   restated the `.ml-sw-cur` rule in its own stylesheet to get the layout
   the library meant to give it. Anything added here has to stay after the
   base rule it overrides — which is what being last guarantees. */

/* ---------- App shell: narrow ---------- */

/* Every inner grid below has a breakpoint; the one container that decides the
   page width had none. At 380px the 252px rail left 128px for panels that need
   358-714, so every console on the platform pushed the page sideways instead of
   reflowing. Below 900px the rail stops being a column and becomes a scrolling
   strip under the topbar — the labels survive, which an icon-only rail would
   have cost. Scrolling lives on .ml-side-nav, not .ml-side: an overflow
   container would clip the service switcher's popup. */

@media (max-width: 900px) {
  /* Block, not a one-column grid: as a grid item the aside keeps
     min-width:auto, so the single track is sized to the rail's min-content
     (846px) and the page is wider than the phone again — the collapse looks
     applied and changes nothing. Out of the grid the strip also stays sticky
     under the topbar, the way the rail does on desktop. */
  .ml-shell { display: block; }
  .ml-side {
    display: flex; align-items: center; gap: 10px;
    min-width: 0;
    height: auto; overflow: visible;
    padding: 8px 14px;
    z-index: 40;
    border-right: 0; border-bottom: 1px solid var(--line-2);
  }
  .ml-sw { flex: none; }
  .ml-sw-trigger { width: auto; padding: 6px 8px; gap: 0; }
  .ml-sw-cur, .ml-sw-chev { display: none; }
  .ml-side-nav {
    flex-direction: row; margin-top: 0; gap: 6px;
    min-width: 0; overflow-x: auto; scrollbar-width: none;
    padding-bottom: 2px;
  }
  .ml-side-nav::-webkit-scrollbar { display: none; }
  .ml-nav-group { display: none; }
  .ml-nav-item {
    flex: none; white-space: nowrap;
    /* 7px on a 13.5px line gave a 34px pill — this is the ONE layout that is
       reached by a finger, and it was the one under the 40px floor. */
    min-height: 40px; padding: 7px 14px; border-radius: var(--r-full);
    background: var(--fill-1);
  }
  .ml-main { padding: 20px 16px 48px; min-width: 0; }

  /* Panel furniture that assumes a wide row. A head is a title plus a badge or
     a link on one line; a badge never wraps by design. Both are right on a
     desktop and both push the page on a phone. */
  .ml-panel-head { flex-wrap: wrap; }
  .ml-badge { white-space: normal; }
  /* .ml-pghead already wraps; its action cluster did not, so a two-button row
     hung off the edge while the head above it reflowed neatly. */
  .ml-pghead-right { flex-wrap: wrap; gap: 8px; }

  /* Same grid-item trap as the shell, one level in: the inner columns collapse
     to 1fr below 860, but the track is still sized to the widest child's
     min-content unless the child is allowed to shrink. */
  .cb-grid-main > *, .cb-grid-sub > * { min-width: 0; }

  /* Same reason, for panels dropped into a page's own grid: a panel that may
     not shrink sizes the whole track to its min-content. */
  .ml-panel { min-width: 0; }

  /* A segmented control is a row of tabs; on a phone it scrolls like one. */
  .cb-seg { max-width: 100%; overflow-x: auto; }
}

/*
  Sheen — diagonal hover gloss sweep, lifted from memalerts .meme-card
  in PR-2.1. Three variants (subtle / standard / pronounced) differ in
  duration + peak amplitude + easing. The keyframe is shared; per-variant
  classes set the variant tokens that the keyframe + ::before consume.

  Apply one variant class on the element you want to gloss:
    <div className="sheen-standard surface">…</div>

  Constraints:
  - Hover-only via @media (hover: hover) and (pointer: fine) — touch
    devices skip the sweep (no fake hover state).
  - The element must be a positioning context — we set position: relative
    explicitly here.
  - The element should clip overflow (overflow: hidden) so the sweep does
    not bleed past corners. Most card-like surfaces (Card, surface) do
    already; if not, the consumer adds it. We do NOT enforce it on the
    utility because that would also clip drop-shadows on adjacent
    children (dropdowns, tooltips inside the card).
  - mix-blend-mode: overlay assumes a non-pure-black/white background.
    On glass/surface tokens it composites correctly.
*/

@keyframes ml-sheen-sweep {
  0% {
    transform: translateX(-120%) skewX(-18deg);
    opacity: 0;
  }
  20% {
    /* CSS variables are resolved at animation start, so each .sheen-*
       variant injects its own peak amplitude via --ml-sheen-amplitude. */
    opacity: var(--ml-sheen-amplitude, 0.85);
  }
  100% {
    transform: translateX(120%) skewX(-18deg);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sheen-subtle::before,
  .sheen-standard::before,
  .sheen-pronounced::before {
    animation: none;
  }
}

@keyframes ml-float {
  0%,
  100% {
    transform: translateY(0px);
  }
  50% {
    transform: translateY(-20px);
  }
}

@keyframes ml-gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

@keyframes ml-pulse-glow {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

@keyframes ml-fade-up {
  0% {
    opacity: 0;
    transform: translateY(30px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes ml-modal-backdrop {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes ml-modal-pop {
  from {
    opacity: 0;
    transform: translateY(6px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes ml-modal-backdrop-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

@keyframes ml-modal-pop-out {
  from {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  to {
    opacity: 0;
    transform: translateY(6px) scale(0.98);
  }
}

@keyframes ml-shimmer {
  0% {
    transform: translateX(-200%);
  }
  100% {
    transform: translateX(200%);
  }
}

@keyframes ml-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

/* Reduced motion — scoped to library classes only, no global !important */

@media (prefers-reduced-motion: reduce) {
  .panel,
  .glass,
  .surface,
  .surface-hover,
  .animated-gradient,
  [class*='animate-float'],
  [class*='animate-gradient'],
  [class*='animate-pulse-glow'],
  [class*='animate-fade-up'],
  [class*='animate-modal'],
  [class*='animate-shimmer'],
  [class*='animate-spin'],
  [class*='animate-pulse'],
  [data-closing] {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
    transition-duration: 0.01ms;
  }
}

*, ::before, ::after {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}

::backdrop {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}

/* ! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com */

/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/

*,
::before,
::after {
  box-sizing: border-box; /* 1 */
  border-width: 0; /* 2 */
  border-style: solid; /* 2 */
  border-color: #e5e7eb; /* 2 */
}

::before,
::after {
  --tw-content: '';
}

/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/

html,
:host {
  line-height: 1.5; /* 1 */
  -webkit-text-size-adjust: 100%; /* 2 */
  -moz-tab-size: 4; /* 3 */
  -o-tab-size: 4;
     tab-size: 4; /* 3 */
  font-family: var(--ml-font-sans); /* 4 */
  font-feature-settings: normal; /* 5 */
  font-variation-settings: normal; /* 6 */
  -webkit-tap-highlight-color: transparent; /* 7 */
}

/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/

body {
  margin: 0; /* 1 */
  line-height: inherit; /* 2 */
}

/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/

hr {
  height: 0; /* 1 */
  color: inherit; /* 2 */
  border-top-width: 1px; /* 3 */
}

/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/

abbr:where([title]) {
  -webkit-text-decoration: underline dotted;
          text-decoration: underline dotted;
}

/*
Remove the default font size and weight for headings.
*/

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: inherit;
  font-weight: inherit;
}

/*
Reset links to optimize for opt-in styling instead of opt-out.
*/

a {
  color: inherit;
  text-decoration: inherit;
}

/*
Add the correct font weight in Edge and Safari.
*/

b,
strong {
  font-weight: bolder;
}

/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/

code,
kbd,
samp,
pre {
  font-family: var(--ml-font-mono); /* 1 */
  font-feature-settings: normal; /* 2 */
  font-variation-settings: normal; /* 3 */
  font-size: 1em; /* 4 */
}

/*
Add the correct font size in all browsers.
*/

small {
  font-size: 80%;
}

/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/

table {
  text-indent: 0; /* 1 */
  border-color: inherit; /* 2 */
  border-collapse: collapse; /* 3 */
}

/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/

button,
input,
optgroup,
select,
textarea {
  font-family: inherit; /* 1 */
  font-feature-settings: inherit; /* 1 */
  font-variation-settings: inherit; /* 1 */
  font-size: 100%; /* 1 */
  font-weight: inherit; /* 1 */
  line-height: inherit; /* 1 */
  letter-spacing: inherit; /* 1 */
  color: inherit; /* 1 */
  margin: 0; /* 2 */
  padding: 0; /* 3 */
}

/*
Remove the inheritance of text transform in Edge and Firefox.
*/

button,
select {
  text-transform: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/

button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
  -webkit-appearance: button; /* 1 */
  background-color: transparent; /* 2 */
  background-image: none; /* 2 */
}

/*
Use the modern Firefox focus style for all focusable elements.
*/

:-moz-focusring {
  outline: auto;
}

/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/

:-moz-ui-invalid {
  box-shadow: none;
}

/*
Add the correct vertical alignment in Chrome and Firefox.
*/

progress {
  vertical-align: baseline;
}

/*
Correct the cursor style of increment and decrement buttons in Safari.
*/

::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
  height: auto;
}

/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/

[type='search'] {
  -webkit-appearance: textfield; /* 1 */
  outline-offset: -2px; /* 2 */
}

/*
Remove the inner padding in Chrome and Safari on macOS.
*/

::-webkit-search-decoration {
  -webkit-appearance: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/

::-webkit-file-upload-button {
  -webkit-appearance: button; /* 1 */
  font: inherit; /* 2 */
}

/*
Add the correct display in Chrome and Safari.
*/

summary {
  display: list-item;
}

/*
Removes the default spacing and border for appropriate elements.
*/

blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
  margin: 0;
}

fieldset {
  margin: 0;
  padding: 0;
}

legend {
  padding: 0;
}

ol,
ul,
menu {
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
Reset default styling for dialogs.
*/

dialog {
  padding: 0;
}

/*
Prevent resizing textareas horizontally by default.
*/

textarea {
  resize: vertical;
}

/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/

input::-moz-placeholder, textarea::-moz-placeholder {
  opacity: 1; /* 1 */
  color: #9ca3af; /* 2 */
}

input::placeholder,
textarea::placeholder {
  opacity: 1; /* 1 */
  color: #9ca3af; /* 2 */
}

/*
Set the default cursor for buttons.
*/

button,
[role="button"] {
  cursor: pointer;
}

/*
Make sure disabled buttons don't get the pointer cursor.
*/

:disabled {
  cursor: default;
}

/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
   This can trigger a poorly considered lint error in some tools but is included by design.
*/

img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
  display: block; /* 1 */
  vertical-align: middle; /* 2 */
}

/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/

img,
video {
  max-width: 100%;
  height: auto;
}

/* Make elements with the HTML hidden attribute stay hidden by default */

[hidden]:where(:not([hidden="until-found"])) {
  display: none;
}

body {
    background-color: rgb(var(--ml-bg, 10 10 15));
    color: rgb(var(--ml-fg, 249 250 251));
    font-family: var(--ml-font-sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

/* Scrollbar */

::-webkit-scrollbar {
    width: 6px;
  }

::-webkit-scrollbar-track {
    background: rgb(var(--ml-surface-50, 15 15 24));
  }

::-webkit-scrollbar-thumb {
    background: rgb(var(--ml-surface-300, 36 36 58));
    border-radius: 9999px;
  }

/* Global focus-visible */

:where(a, button, input, select, textarea, [role='button'], [role='menuitem'], [role='tab']):focus-visible {
    outline: none;
    box-shadow:
      0 0 0 2px rgb(var(--ml-primary, 139 92 246)),
      0 0 0 4px transparent;
  }

/* Dark select */

select {
    color-scheme: dark;
  }

select option,
  select optgroup {
    background-color: rgb(var(--ml-surface-50, 15 15 24));
    color: rgb(var(--ml-fg, 249 250 251));
  }

/* Accessibility: link underline offset */

a {
    text-underline-offset: 0.18em;
  }

.container {
  width: 100%;
}

@media (min-width: 640px) {

  .container {
    max-width: 640px;
  }
}

@media (min-width: 768px) {

  .container {
    max-width: 768px;
  }
}

@media (min-width: 1024px) {

  .container {
    max-width: 1024px;
  }
}

@media (min-width: 1280px) {

  .container {
    max-width: 1280px;
  }
}

@media (min-width: 1536px) {

  .container {
    max-width: 1536px;
  }
}

/*
   * Opaque panel: warm-charcoal surface + hairline border. No blur, no
   * translucency — this has not been glassmorphism since the Console rework.
   *
   * `.glass` is the old name, kept as an alias so no consumer breaks. It is
   * deprecated and should not be written in new code: a class that describes a
   * material it does not render sends every reader — grep, human and model — to
   * the wrong conclusion, and it has already produced one false «charter
   * violation» that cost a real investigation. Measure the pixel, not the name.
   */

.panel,
  .glass {
    border-radius: var(--ml-radius-md, 0.625rem);
    background: rgb(var(--ml-surface-50, 23 21 19));
    box-shadow:
      var(--ml-shadow-surface),
      inset 0 0 0 1px var(--ml-glass-border, rgba(255, 255, 255, 0.07));
  }

/*
   * `.panel` is a base style for non-interactive panel containers
   * (dropdown popups, sticky navbars, sidebars, modal bodies, error-boundary
   * panels). It does NOT carry a hover state — interactive children own their
   * own hover. Hover on a panel is opt-in via `.surface-hover` (below) or
   * Card's `hoverable` prop, which uses `.surface-hover`.
   *
   * Historical: a `.glass:hover` rule used to live here and applied globally,
   * causing the entire panel background to shift when the cursor entered the
   * popup — most visible on UserMenu and other dropdowns. Removed deliberately
   * (Modal/AppModal previously opted out via `bg-surface-50/80` instead of
   * `glass`; that workaround is now obsolete). If you need a hover, layer
   * `.surface-hover` on top of `.panel` explicitly.
   */

/*
   * Hit target — 40×40, drawn at whatever size the design wants.
   *
   * How big a control LOOKS and how big it is to HIT are different questions.
   * 40px is the platform floor below which a thumb starts missing; growing the
   * boxes to match would move layout in every consumer, so grow only the
   * target. Same trick as `.ml-iconbtn::after` and `.ml-switch::before`, made
   * reusable because the components written in Tailwind had no way to reach it.
   *
   * The skirt is centred on the control, so put it on the control itself, and
   * check neighbours: two of these 8px apart will have their skirts meet.
   *
   * TWO REQUIREMENTS ON THE CALLER, both of which fail silently:
   *
   * 1. The control must already establish a containing block — `relative`,
   *    `absolute` or `fixed`. This class deliberately does NOT set `position`
   *    itself: `@layer` does not survive the package's postcss build, so a
   *    `position: relative` here would sit unlayered at the same specificity as
   *    Tailwind's `absolute` utility and the winner would be decided by which
   *    stylesheet the consumer happened to import first. Losing that race turns
   *    an overlaid clear button into a block in the layout flow.
   * 2. Nothing interactive within 20px, or this steals its taps.
   *
   * `scripts/rendered-invariants.mjs` asserts both — including a static control
   * whose skirt must NOT land, so requirement 1 stays an executable fact.
   */

/*
   * The anchor a Tooltip builds for itself when its child is a disabled
   * control. A disabled control is absent from sequential focus, so the
   * explanation of WHY it is disabled had no route to a keyboard at all; this
   * wrapper is focusable and becomes the trigger.
   *
   * `inline-flex` with no intrinsic size makes it measure exactly the child it
   * holds, which is what lets it appear and disappear with the disabled state
   * without moving the row — proved in `scripts/rendered-invariants.mjs`, not
   * assumed. It deliberately does NOT neutralise pointer events on the child:
   * measured in Chromium, `mouseenter` and `pointerdown` reach an ancestor of a
   * disabled button on their own, and a rule that changes nothing is a rule
   * the next reader has to disprove.
   */

.tooltip-disabled-anchor {
    display: inline-flex;
  }

.hit-target::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 40px;
    height: 40px;
    transform: translate(-50%, -50%);
  }

/* Surface card — flat opaque (was translucent blur) */

.surface {
    border-radius: var(--ml-radius-md, 0.625rem);
    background: rgb(var(--ml-surface-50, 23 21 19));
    box-shadow: var(--ml-shadow-surface);
  }

.surface-hover {
    transition:
      box-shadow var(--ml-transition-fast, 150ms ease),
      transform var(--ml-transition-fast, 150ms ease);
  }

.surface-hover:hover {
    transform: translateY(-1px);
    box-shadow: var(
      --ml-shadow-surface-hover,
      0 10px 25px rgba(0, 0, 0, 0.45),
      inset 0 0 0 1px rgba(255, 255, 255, 0.09)
    );
  }

/* Section title — heading inside a card */

/* Gradient text */

.text-gradient {
    background: linear-gradient(
      135deg,
      rgb(var(--ml-accent, 102 126 234)) 0%,
      rgb(var(--ml-glow-purple, 118 75 162)) 50%,
      rgb(var(--ml-glow-pink, 240 147 251)) 100%
    );
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
  }

/* Animated gradient background */

.animated-gradient {
    background: linear-gradient(
      270deg,
      rgb(var(--ml-accent, 102 126 234)),
      rgb(var(--ml-glow-purple, 118 75 162)),
      rgb(var(--ml-glow-pink, 240 147 251))
    );
    background-size: 400% 400%;
    animation: ml-gradient 15s ease infinite;
  }

/* Page container */

/* Decorative orbs — REMOVED in MemeLab Console (no ambient blur fields).
     Hidden at the class level so PageShell's ambient orbs vanish without a
     component edit. Definitions kept as no-ops for any direct consumer. */

.orb,
  .orb-purple,
  .orb-blue,
  .orb-pink {
    display: none;
  }

/* iOS safe-area */

.pb-safe {
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
  }

/* Hide scrollbar */

.no-scrollbar::-webkit-scrollbar {
    display: none;
  }

.no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }

/* Soft link */

/*
   * Material ontology — token-driven surfaces.
   *
   * Tokens live in `tokens/materials.css` (`--ml-mat-{material}-{property}`).
   * These classes are the consumers — adding them now unblocks services that
   * want to opt into the 4-material model. Existing `.glass` / `.surface`
   * remain unchanged for backward compatibility.
   *
   * - `.mat-surface`    standard card / content area
   * - `.mat-instrument` interactive control panel (button rows, sliders, knobs)
   * - `.mat-console`    dense data table / sidebar / log viewer (darkest)
   * - `.mat-marquee`    display / hero / spotlight (lightest, broadcast-graphic)
   */

/* Materials flattened to opaque charcoal steps (no blur). Each maps to a
     surface rung so the 4-material API keeps working under the flat language. */

.mat-surface {
    border-radius: var(--ml-radius-md, 0.625rem);
    background: rgb(var(--ml-surface-50, 23 21 19));
    box-shadow: var(--ml-shadow-surface);
  }

.mat-instrument {
    border-radius: var(--ml-radius-md, 0.625rem);
    background: rgb(var(--ml-surface-100, 31 28 25));
    box-shadow: var(--ml-shadow-surface);
  }

.mat-console {
    border-radius: var(--ml-radius-sm, 0.4375rem);
    background: rgb(var(--ml-inset, 16 15 13));
    box-shadow: var(--ml-shadow-surface);
  }

.mat-marquee {
    border-radius: var(--ml-radius-lg, 0.875rem);
    background: rgb(var(--ml-surface-200, 40 36 32));
    box-shadow: var(--ml-shadow-surface);
  }

/* MemeLab Console: the hover gloss sweep is removed (flat language, no gloss).
     ::before is suppressed so the .sheen-* utilities become no-ops while the
     class names remain valid on consumers. */

/* Per-variant from-color selection.

     All three are neutral white now. The `standard` and `pronounced`
     variants used to pick up a per-channel tint from --ml-ch-sheen-*, which
     was removed in 0.28.0 along with the rest of the channel palette: the
     sweep itself is suppressed above (flat language, no gloss), so the tint
     was colouring a gradient that never drew. The custom properties stay as
     the extension point if a tinted sweep is ever wanted again. */

/* Dark mode tints down — the same 0.55 alpha would blow out on dark
     surfaces. */

.dark .sheen-subtle::before,
  .dark .sheen-standard::before,
  .dark .sheen-pronounced::before {
    --ml-sheen-tint: 0.25;
  }
