@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..900;1,8..60,300..900&family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap');

/* ══════════════════════════════════════════════════════════════
   Keln — typography, themes and density.

   ══ THE TYPOGRAPHY BUG THIS FILE FIXES ══════════════════════

   `--font` and `--mono` were never defined. Not here, not in
   theme.js, not anywhere in the repo. Every rule in Keln that
   said `font-family: var(--font)` was therefore an INVALID
   declaration, which the browser discards — falling back to its
   default serif.

   That is where the serif on the dashboard, FAQ, support and
   changelog came from. It was never chosen. It was the browser's
   Times New Roman showing through a hole in the token layer.

   The landing and auth pages escaped it only because they wrote
   `var(--font, 'Inter', system-ui, sans-serif)` WITH a fallback,
   so they got Inter — which is why those two pages have never
   matched the rest of the product.

   The accidental serif was the better instinct. But an undefined
   token renders differently on every platform: Times New Roman on
   Windows, Times on macOS, Liberation Serif on most Linux, and
   something else again on Android. Nobody was seeing the same
   site. So the serif is now PINNED and real.

   ══ THE STACK ═══════════════════════════════════════════════

   SOURCE SERIF 4 — Adobe, and the reason it wins here is optical
   sizing. It is a variable font with an `opsz` axis, so the same
   family thins its strokes and tightens its spacing for a 60px
   headline and thickens them for an 11px sidebar label. Times has
   one drawing for both, which is why Times headlines look fine
   and Times UI at 11px on a dark background disappears — thin
   strokes on dark backgrounds are eaten by the display.

   Everything in Keln is 11–14px on near-black. That single fact
   picks the typeface.

   ══ THE DIVISION OF LABOUR ══════════════════════════════════

     --font   the serif. Anything a human READS: headings, body,
              labels, nav, buttons, prose.
     --mono   anything a machine PRODUCED: counts, currency,
              dates, eyebrows, stage labels, keyboard hints, IDs.
     --sans   escape hatch. The title screen and any surface that
              must not be serif. Not used for body copy anywhere.

   That division is what makes Keln read as an instrument rather
   than as a blog with a dark theme. The serif carries the voice;
   the mono carries the readout. Mixing them is the whole idea —
   a page set entirely in one or the other loses the distinction
   between what Keln says and what Keln measured.

   ══ TWO INDEPENDENT AXES ════════════════════════════════════

     data-theme    deep | slate | graphite     what it looks like
     data-density  full | minimal              how much is shown

   Conflating them is the usual mistake. Someone can want the sky
   gone but love the dark glass, or want full XP in a quieter
   palette. One switch controlling both would force a choice
   nobody asked to make.

   THEMES ARE PURE TOKEN OVERRIDES. Every surface in Keln already
   reads from custom properties, so a palette is ~20 lines and zero
   component changes. That is the payoff for having been disciplined
   about tokens from the start.

   MINIMAL REMOVES THE GAME, NOT THE CRAFT. The objection was never
   the dark glass — it was being scored and nudged. Linear and
   Superhuman are minimal and have loud aesthetics. Minimal does not
   mean neutral, and Keln in Minimal should still be unmistakably
   Keln.

   ══ ON THE @import AT THE TOP ═══════════════════════════════
   Every page in Keln links /theme.css, and none of them agree on
   what else they link. Putting the font request here is the only
   single-file change that reaches the whole product at once.

   It costs one serialised round trip: theme.css must arrive
   before the font CSS is even requested. To buy that back on any
   page you care about, add these two lines to its <head> ABOVE
   the theme.css link and the @import becomes a cache hit:

     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,300..900;1,8..60,300..900&family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap">

   landing.html already does this. Do the rest as you touch them —
   there is no flag day, the @import covers anything you miss.
   ══════════════════════════════════════════════════════════════ */

/* ─── TYPOGRAPHY TOKENS ────────────────────────────────────────
   Declared on :root ONLY — never inside a [data-theme] block.
   A theme changes colour, not voice. */
:root {
  --serif:
    'Source Serif 4', 'Source Serif Pro',
    /* Platform serifs worth having if the webfont never lands.
       Iowan and Charter are genuinely good screen serifs; Georgia
       is the reliable floor; Times is the last resort. */
    'Iowan Old Style', Charter, 'Bitstream Charter',
    Georgia, 'Liberation Serif', 'Times New Roman', serif;

  --sans:
    Inter, system-ui, -apple-system, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;

  --mono:
    'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono',
    Menlo, Consolas, 'Liberation Mono', monospace;

  /* The product voice. Every `var(--font)` in the codebase —
     and there are hundreds — resolves through this one line. */
  --font: var(--serif);
}

/* ══════════════════════════════════════════════════════════════
   THE DIMENSIONAL SYSTEM

   Keln's COLOUR system was already good: tokens, three themes, per
   stage colours, an intensity multiplier the canvas layers read. Its
   dimensional system did not exist. Measured across 42 stylesheets:

     22 distinct border-radii      289 uses
     35 distinct font-sizes        530 uses
     39 distinct spacing values  1,040 uses

   Nobody chose 22 radii. They accumulated, one reasonable local
   decision at a time, across a dozen batches — and the result is
   that two panels built a month apart are subtly, unaccountably
   different, which is exactly the feeling of software that is not
   quite finished.

   ─── HOW THESE NUMBERS WERE PICKED ──────────────────────────

   Not from taste. From the existing distribution: the scale is the
   set of values the product already clusters around, so converting
   is rounding rather than redesign. Every value in the codebase
   moves by at most 2px. That is the whole reason this is safe to do
   in one pass — a system imposed from outside would have required
   redrawing every surface to match it.

   ─── AND WHAT IS DELIBERATELY NOT IN THE SCALE ──────────────

   Anything above the top step. A 68px landing headline and a 999px
   pill are not drift, they are decisions, and a scale that swallowed
   them would be a scale that flattens the product. The system
   governs the crowded middle — 8 to 24px — which is where
   inconsistency actually lives and where nobody can tell 9 from 10.
   ══════════════════════════════════════════════════════════════ */

:root {
  /* ── Radius ────────────────────────────────────────────────
     Read as: how soft is this corner, at this size of element.
     Chips and rails are hairline; cards are lg; overlays are xl. */
  --r-hair: 2px;    /* rails, bars, the thinnest marks           */
  --r-xs:   4px;    /* tags, tiny chips, inline code             */
  --r-sm:   8px;    /* buttons, inputs, list rows                */
  --r-md:  12px;    /* cards, panels, most containers            */
  --r-lg:  16px;    /* large cards, the spotlight cutout         */
  --r-xl:  20px;    /* dialogs, sheets                           */
  --r-2xl: 24px;    /* the biggest surfaces                      */
  --r-pill: 999px;  /* anything whose ends should be semicircles */

  /* ── Type ──────────────────────────────────────────────────
     Ten steps, and the four in the middle carry most of the
     product. The gaps widen as they climb because a 1px difference
     matters at 10px and is invisible at 24px. */
  --t-2xs: 10px;    /* eyebrows, counts, the smallest mono       */
  --t-xs:  11px;    /* meta, timestamps, table footnotes         */
  --t-sm:  12px;    /* secondary UI, dense labels                */
  --t-base:13px;    /* the default. Most of the product.         */
  --t-md:  14px;    /* body text people actually read            */
  --t-lg:  15px;    /* lead paragraphs                           */
  --t-xl:  16px;    /* small headings                            */
  --t-2xl: 18px;    /* card titles                               */
  --t-3xl: 21px;    /* panel and dialog titles                   */
  --t-4xl: 24px;    /* section headings                          */
  /* Above this, sizes are one-offs and belong to their surface. */

  /* ── Space ─────────────────────────────────────────────────
     Padding, margin and gap. Not a strict 4-multiple: the real
     distribution clusters at 6, 10 and 14 as hard as at 8 and 12,
     and forcing those onto a 4-grid would move more pixels than it
     would tidy. */
  --s-1:   2px;
  --s-2:   4px;
  --s-3:   6px;
  --s-4:   8px;
  --s-5:  10px;
  --s-6:  12px;
  --s-7:  14px;
  --s-8:  16px;
  --s-9:  20px;
  --s-10: 24px;
  --s-11: 28px;
  --s-12: 32px;

  /* ── Elevation ─────────────────────────────────────────────
     Four heights, each a shadow AND a border, because in a dark
     product a shadow alone does not separate a surface from the one
     behind it — the edge is what does the work.

     Scaled by --glow, so the quieter themes lift less. */
  --e-flat:  none;
  --e-raise: 0 2px 8px rgba(0, 0, 0, calc(.18 * var(--glow)));
  --e-card:  0 10px 30px rgba(0, 0, 0, calc(.34 * var(--glow)));
  --e-float: 0 20px 56px rgba(0, 0, 0, calc(.46 * var(--glow)));
  --e-over:  0 32px 80px rgba(0, 0, 0, calc(.58 * var(--glow)));

  /* ── Motion ────────────────────────────────────────────────
     Three durations and two curves, so that everything in the
     product accelerates the same way.

     `--ease-out` is for things ARRIVING — it decelerates into
     place, which reads as the object settling. `--ease-spring`
     overshoots slightly and is reserved for things the user caused
     directly. Nothing ambient is allowed to use it; a page that
     springs at you on its own is a page that feels nervous. */
  --d-fast:  .16s;   /* hover, focus, colour changes             */
  --d-base:  .28s;   /* panels opening, content swapping         */
  --d-slow:  .5s;    /* the spotlight travelling, big reveals    */
  --ease-out:    cubic-bezier(.22, 1, .36, 1);
  --ease-spring: cubic-bezier(.2, .9, .3, 1.2);
}

/* Motion tokens collapse rather than every rule needing its own
   reduced-motion branch. One place to honour the preference. */
@media (prefers-reduced-motion: reduce) {
  :root { --d-fast: 0s; --d-base: 0s; --d-slow: 0s; }
}

/* Let the variable font actually use its optical-size axis. Without
   this, Source Serif 4 renders at a single optical size and most of
   the reason for choosing it is thrown away. */
html {
  font-optical-sizing: auto;
}

/* Serif numerals default to old-style in some stacks, which makes
   a column of figures ripple. Anything numeric should line up. */
.mono,
[class*="__num"],
[class*="-num"] {
  font-variant-numeric: tabular-nums lining-nums;
}
.mono { font-family: var(--mono); }

/* ── Icons ──────────────────────────────────────────────────── */
.ico {
  display: inline-block;
  width: 1.2em;
  height: 1.2em;
  vertical-align: middle;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
}
.ico--grad {
  stroke: url(#i-grad) !important;
}
.ico--fill {
  fill: currentColor;
  stroke: none;
}
.ico--inline {
  display: inline;
  vertical-align: text-bottom;
}
.ico--sm {
  width: 0.9em;
  height: 0.9em;
}
.ico--lg {
  width: 1.6em;
  height: 1.6em;
}
.ico--xl {
  width: 2.2em;
  height: 2.2em;
}
.ico--spin {
  animation: ico-spin 1.2s linear infinite;
}
.vega-wordmark {
  font-family: var(--vega-wordmark-font);
  font-weight: var(--vega-wordmark-weight);
  letter-spacing: var(--vega-wordmark-letter-spacing);
}
@keyframes ico-spin {
  to { transform: rotate(360deg); }
}

/* ─── STAGE COLOURS AS TOKENS ──────────────────────────────────
   Previously hardcoded in three JS files. Promoting them to tokens
   is what lets a theme restyle the orbit, the calendar and the
   dossier at once — and it removes the drift risk of three copies. */
:root,
[data-theme="deep"] {
  --bg:        #0a0e14;
  --bg2:       #0d1117;
  --panel:     rgba(22,27,34,.72);
  --border:    rgba(91,140,255,.16);
  --border-hi: rgba(34,211,166,.5);
  --ink:       #e9f0f7;
  --muted:     #8b9bb0;
  --dim:       #5a6b80;
  --accent:    #5b8cff;
  --accent2:   #22d3a6;
  --accent3:   #00e0c6;
  --gold:      #ffd166;
  --hot:       #ff6b8b;
  --warn:      #ffb454;

  --stage-lead:      #5a6b80;
  --stage-contacted: #5b8cff;
  --stage-replied:   #3fb9d4;
  --stage-call:      #22d3a6;
  --stage-proposal:  #ffd166;
  --stage-won:       #00e0c6;
  --stage-lost:      #ff6b8b;

  --glow: 1;          /* multiplier the JS layers read for shadow intensity */

  /* Vega's name is set in mono everywhere it appears, so the word
     reads as a system entity rather than as a person. That contrast
     only works now that the surrounding text is serif. */
  --vega-wordmark-font: var(--mono);
  --vega-wordmark-weight: 600;
  --vega-wordmark-letter-spacing: .06em;
}

/* SLATE — the same architecture, cooler and quieter. Less blue
   bloom, a muted teal. For people who find the default too electric
   without wanting to give up the aesthetic entirely. */
[data-theme="slate"] {
  --bg:        #0e1116;
  --bg2:       #12161d;
  --panel:     rgba(28,34,43,.74);
  --border:    rgba(140,160,190,.14);
  --border-hi: rgba(126,178,168,.42);
  --ink:       #e4e9ef;
  --muted:     #96a3b3;
  --dim:       #667486;
  --accent:    #7c9cc4;
  --accent2:   #7eb2a8;
  --accent3:   #8fc4bb;
  --gold:      #d9b878;
  --hot:       #d68a9a;
  --warn:      #d9b878;

  --stage-lead:      #667486;
  --stage-contacted: #7c9cc4;
  --stage-replied:   #78a6b4;
  --stage-call:      #7eb2a8;
  --stage-proposal:  #d9b878;
  --stage-won:       #8fc4bb;
  --stage-lost:      #d68a9a;

  --glow: .6;
}

/* GRAPHITE — near-monochrome. Stage colour becomes a VALUE ramp
   rather than a hue ramp, so the ordering still reads (lighter =
   further along) without any colour at all. That is the honest way
   to do monochrome: preserve the information, drop the decoration. */
[data-theme="graphite"] {
  --bg:        #0c0c0e;
  --bg2:       #101012;
  --panel:     rgba(26,26,30,.78);
  --border:    rgba(160,160,170,.12);
  --border-hi: rgba(200,200,210,.34);
  --ink:       #e8e8ea;
  --muted:     #9a9aa2;
  --dim:       #66666e;
  --accent:    #a8adb6;
  --accent2:   #c8ccd4;
  --accent3:   #dcdfe4;
  --gold:      #b9b3a4;
  --hot:       #c49aa2;
  --warn:      #b9b3a4;

  --stage-lead:      #55555c;
  --stage-contacted: #6e6e77;
  --stage-replied:   #888891;
  --stage-call:      #a2a2ab;
  --stage-proposal:  #bcbcc4;
  --stage-won:       #dcdfe4;
  --stage-lost:      #c49aa2;   /* the one hue kept: lost must read as different, not just dimmer */

  --glow: .25;
}

/* ══════════════════════════════════════════════════════════════
   MINIMAL DENSITY

   Everything below hides or calms. Nothing is deleted from the DOM
   by CSS alone — the JS layer also skips the work (see theme.js), so
   Minimal is genuinely cheaper to run, not just visually quieter.
   ══════════════════════════════════════════════════════════════ */

/* ── Gone: the scoreboard ──────────────────────────────────── */
[data-density="minimal"] .sky-hero,
[data-density="minimal"] [data-section="constellation"],
[data-density="minimal"] .chip--xp,
[data-density="minimal"] .chip--streak,
[data-density="minimal"] .lvl-ring,
[data-density="minimal"] .xp-bar,
[data-density="minimal"] .badge-grid,
[data-density="minimal"] .reward-pane[data-pane="badges"],
[data-density="minimal"] .sidebar__meta,
[data-density="minimal"] #patternCard .pat__t { display: none !important; }

/* Plays survive — the CONTENT is useful, only the unlock framing
   was the problem. They read as a playbook index instead. */
[data-density="minimal"] .unlock__lock,
[data-density="minimal"] .unlock__lvl { display: none !important; }
[data-density="minimal"] .unlock { opacity: 1 !important; }

/* ── Gone: celebration ─────────────────────────────────────── */
[data-density="minimal"] .confetti,
[data-density="minimal"] .kb,
[data-density="minimal"] .asc-gate,
[data-density="minimal"] .cheat-fx { display: none !important; }

/* ── Calmer: motion ────────────────────────────────────────── */
[data-density="minimal"] * {
  animation-duration: .01ms !important;
  animation-iteration-count: 1 !important;
}
[data-density="minimal"] .es__glyph::after,
[data-density="minimal"] .vega-probe__ring,
[data-density="minimal"] .ds__orb { animation: none !important; }

/* Vega docks instead of drifting. Still Vega, less toy. */
[data-density="minimal"] .vega-probe {
  transform: none !important;
  left: 24px; bottom: 24px; top: auto; cursor: pointer;
}

/* ── Tighter: density ──────────────────────────────────────── */
[data-density="minimal"] .glass { padding: 18px; border-radius: 14px; }
[data-density="minimal"] .card-t { font-size: 13px; margin-bottom: 12px; }
[data-density="minimal"] .grid { gap: 14px; }

/* ── Quieter: glow ─────────────────────────────────────────── */
[data-density="minimal"] .sidebar,
[data-density="minimal"] .ds-card,
[data-density="minimal"] .cal {
  box-shadow: 0 14px 40px rgba(0,0,0,.45) !important;
}
[data-density="minimal"] .sidebar::before { opacity: .25; }

/* ══════════════════════════════════════════════════════════════
   THE SWITCHER
   ══════════════════════════════════════════════════════════════ */
.themes { display: grid; gap: 10px; }

.theme-row {
  display: flex; align-items: center; gap: 13px;
  padding: 13px 15px; border-radius: 13px;
  background: rgba(6,10,16,.45);
  border: 1px solid var(--border);
  cursor: pointer; text-align: left; width: 100%;
  font-family: var(--font);
  transition: border-color .2s, background .2s;
}
.theme-row:hover { border-color: var(--border-hi); }
.theme-row--on { border-color: var(--border-hi); background: rgba(34,211,166,.06); }
.theme-row:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

/* A live swatch built from the theme's own tokens, so it can never
   misrepresent what selecting it will do. */
.theme-sw { display: flex; gap: 3px; flex-shrink: 0; }
.theme-sw i {
  width: 13px; height: 26px; border-radius: 3px; display: block;
}

.theme-row__b { flex: 1; min-width: 0; }
.theme-row__n { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.theme-row__d { font-size: 11.5px; color: var(--dim); margin-top: 2px; }
.theme-row__c {
  width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;
  border: 1px solid var(--border); display: grid; place-items: center;
  font-size: 10px; color: transparent;
}
.theme-row--on .theme-row__c {
  background: var(--accent2); border-color: var(--accent2); color: #04121a;
}

.density-note {
  font-size: 12px; color: var(--dim); line-height: 1.55;
  margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--border);
}

/* ══════════════════════════════════════════════════════════════
   HIGH CONTRAST
   ══════════════════════════════════════════════════════════════
   A third axis, not a fourth theme: it overrides the readability
   tokens and leaves the palette's identity alone, so Slate at high
   contrast is still recognisably Slate.

   Three things change, and only three:

     1. TEXT. --muted and --dim exist to make secondary text recede.
        At high contrast they stop receding — everything meets the
        foreground, and hierarchy is carried by size and weight,
        which survive a contrast requirement in a way that opacity
        does not.

     2. SURFACES. The glass panels are translucent, which means the
        text on them sits over whatever the starfield is doing
        underneath. Solid panels remove that variable entirely.

     3. EDGES. Borders go from a hint to a line, because at high
        contrast a boundary you have to infer is not a boundary.

   The starfield is dimmed rather than removed: it is behind
   everything, and at 12% it is texture rather than noise. */
[data-contrast="high"] {
  --panel:     rgba(4, 6, 10, .97);
  --border:    rgba(233, 240, 247, .42);
  --border-hi: rgba(233, 240, 247, .8);
  --muted:     #dfe7f0;
  --dim:       #c3cedb;
  --glow:      0;
}

[data-contrast="high"] body::before,
[data-contrast="high"] .keln-atmos { opacity: .12; }

/* Focus has to be unmissable, not merely present. */
[data-contrast="high"] :focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}

/* Stage colours stay — they are useful — but at high contrast they
   stop being the only thing distinguishing one stage from another.
   Anything already carrying a readable stage name needs nothing;
   what needed help was the bare dot, so it gets a ring it can be
   told apart by. */
[data-contrast="high"] .agenda__dot,
[data-contrast="high"] .cal-ev__dot {
  outline: 1px solid var(--ink);
  outline-offset: 1px;
}

/* ── Forced colours ────────────────────────────────────────────
   Windows High Contrast and similar replace the palette wholesale.
   Translucent panels and glow have no meaning there, and a glass
   edge drawn in a colour the OS overrode is an edge that vanishes.
   Restating the structural borders in system colours keeps the
   panel boundaries visible whatever palette is imposed. */
@media (forced-colors: active) {
  .glass,
  .kcv-panel,
  .theme-row,
  .ds-card { border: 1px solid CanvasText; }

  /* Decoration that carries no information and cannot be recoloured
     usefully. */
  body::before,
  .keln-atmos { opacity: .06; }
}
