/* Semantic tokens only. A raw colour value anywhere outside this file is a
   bug. Both themes are first class: light is not bolted on later, and a
   calendar page carries a publisher-chosen theme independent of the visitor's
   OS setting.

   Radius and border-width tokens are deliberately absent rather than zeroed.
   A component cannot reach for a value that does not exist; structure comes
   from the surface ramp and from hairlines. */

:root {
  /* type */
  --font-sans: 'Satoshi', system-ui, -apple-system, sans-serif;
  --font-display: 'Chillax', 'Satoshi', system-ui, sans-serif;

  /* Two weights carry the whole product. A third would flatten the distance
     between them, which is what the size scale is for. --wght-hover is the
     variable-font swell and appears in no static state. */
  --wght-light: 300;
  --wght-medium: 500;
  --wght-hover: 760;

  /* Only display sizes scale with the viewport. Everything from the event
     title down is a list size: changing it would change line density between
     phone and desktop. */
  --text-display: clamp(2.75rem, 8vw, 4.5rem);
  --text-wordmark: clamp(1.75rem, 6vw, 2rem);
  --text-2xl: 1.6875rem;
  --text-lg: 1.3125rem;
  --text-base: 1rem;
  --text-sm: 0.9375rem;
  --text-xs: 0.875rem;
  --text-2xs: 0.75rem;
  --text-3xs: 0.6875rem;

  --leading-display: 0.98;
  --leading-tight: 1.14;
  --leading-normal: 1.5;

  --track-display: -0.035em;
  --track-title: -0.015em;
  --track-label: 0.12em;
  --track-mark: 0.28em;

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

  /* measure and objects */
  /* One measure for every surface, deliberately. A calendar that jumped wider
     than the list above it read as two different pages, so the grid keeps the
     content width and the content width went up instead.
     40rem was too tight for everything that is not prose: a seven-column month
     grid got 84px per column. 48rem gives it 102px, which fits every seeded
     name but the longest — and the hover card is what carries the full title,
     time and venue anyway.
     The cost is on the prose, and it is real: body text runs ~95 characters
     per line here against the 45–75 that reads comfortably. Accepted for now
     because the text-heavy pages are legal ones nobody reads end to end. If
     that starts to matter, the fix is a max-width on paragraphs rather than a
     second measure — narrow text inside a wide page, not a narrower page. */
  --measure: 48rem;
  --measure-form: 22rem;
  --img-w: 84px;
  --img-ratio: 4 / 5;
  --tap: 44px;
  --marker: 6px;
  /* The height of the sticky header block (site-top). Set per breakpoint by
     base.css so downstream sticky elements can offset themselves. */
  --header-h: 0px;

  /* motion */
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --dur-fast: 120ms;
  --dur-surface: 220ms;
  --dur-type: 260ms;
  --dur-image: 460ms;
}

/* Each surface step has exactly one job. A component that wants "a slightly
   different grey" takes one of these rather than adding a step. */
:root,
[data-theme="dark"] {
  --surface-00: #131314;
  --surface-05: #1a1a1a;
  --surface-10: #1f1f22;
  --surface-15: #242427;
  --surface-20: #2a2a2e;
  --line: #2e2e33;
  --line-strong: #3a3a40;
  --text: #f0f0f0;
  --text-muted: #9c9ca4;
  --text-faint: #8a8a94;
  --accent: #7066d0;
  --invert-surface: #f0f0f0;
  --invert-text: #1a1a1a;
  /* The one green in the product, and it exists because one control is
     genuinely binary: the free filter is on or it is not. It flips with the
     theme the way --invert-surface does, rather than being one colour asked to
     work on both. Two boundaries to clear and it clears them: 8.0:1 against
     the --surface-00 pill around it, 7.49:1 against the --invert-text knob on
     top of it. Never the only signal — the knob moves, and a position reads
     for somebody a hue does not. */
  --accent-positive: #4fbe86;
  --danger: #ff7a6e;
  /* The backdrop behind the share sheet and the lightbox. Deeper than any
     surface step on purpose: it is not another layer of the page, it is the
     page being pushed away. Carries its own alpha rather than an opacity on
     the element, so the panel above it stays fully opaque. */
  --overlay: #08080ab8;
  /* The map's one hue. Water is the single thing on a city outline that a
     reader has to tell apart from land at a glance, and a fifth grey would not
     do it — the Rhine is what the whole view orients by. Everything else on
     the map takes an existing step: land is a surface, boundaries are --line,
     dots are --accent. */
  --map-water: #223142;
  /* MapLibre draws its zoom control glyphs as dark SVGs it ships itself. Not a
     colour of ours to set — a filter is the only lever a foreign icon offers,
     and it belongs beside the theme it depends on rather than in map.css. */
  --map-ctrl-filter: invert(1);
}

/* #e2e2e2 caps the available contrast at 16:1 where white would allow 21:1,
   so the muted and faint steps sit closer together here than they do on dark.
   --text-faint is the lightest value that still clears AA on this base.

   The ramp descends, because on a light interface a raised surface reads
   darker. It descends *monotonically* and every step is distinct: the previous
   values rose once (#dbdbdb) and then fell back until --surface-20 was
   byte-identical to --surface-00, so the most-raised and the most-recessed step
   were the same colour and two of the five steps did not exist.

   The band is 226 → 211 and both ends are fixed. #e2e2e2 is the ground the
   publisher accent band and --text-faint are both measured against, and
   #d3d3d3 is the darkest surface --text-muted still clears AA on (4.56:1) —
   one step further down and the day numbers in the month grid fall below the
   floor. Four roughly even steps is what fits inside it.

   --surface-00 is the one place light cannot mirror dark. On dark it sits below
   the ground and the raised steps sit above it, so sunken and raised move in
   opposite directions. Light has nothing above the ground to move into, so the
   sunken step goes down as well and lands past --surface-20 at the deep end.
   That asymmetry is the contrast ceiling, not an oversight — see DESIGN.md. */
[data-theme="light"] {
  --surface-00: #d3d3d3;
  --surface-05: #e2e2e2;
  --surface-10: #dddddd;
  --surface-15: #d9d9d9;
  --surface-20: #d6d6d6;
  --line: #c9c9c9;
  --line-strong: #b0b0b4;
  --text: #141416;
  --text-muted: #5a5a62;
  --text-faint: #626268;
  --accent: #7066d0;
  --invert-surface: #141416;
  --invert-text: #e2e2e2;
  /* Dark where the dark theme's is light, for the same reason --invert-surface
     flips. 4.99:1 against the --invert-text knob, 4.32:1 against the
     --surface-00 pill — both are boundaries between shapes rather than text on
     a background, so 3:1 is the floor and both clear it comfortably. */
  --accent-positive: #1f6b45;
  --danger: #a3241c;
  /* Lighter alpha than on dark. The same 0.72 over a #d3d3d3 page reads as a
     black sheet rather than as a dimmed one, and the flyer in the lightbox is
     what has to stay the brightest thing on screen. */
  --overlay: #14141699;
  /* Same job on the other side of the ramp: darker than the land it sits in,
     where on dark it is lighter, so water reads as the recessed thing in both. */
  --map-water: #b8c4d0;
  --map-ctrl-filter: none;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --surface-00: #d3d3d3;
    --surface-05: #e2e2e2;
    --surface-10: #dddddd;
    --surface-15: #d9d9d9;
    --surface-20: #d6d6d6;
    --line: #c9c9c9;
    --line-strong: #b0b0b4;
    --text: #141416;
    --text-muted: #5a5a62;
    --text-faint: #626268;
    --accent: #7066d0;
    --invert-surface: #141416;
    --invert-text: #e2e2e2;
    --accent-positive: #1f6b45;
    --danger: #a3241c;
    --overlay: #14141699;
    --map-water: #b8c4d0;
    --map-ctrl-filter: none;
  }
}

/* Publisher accents for calendar pages. Selected by attribute rather than by
   an inline style, because the production CSP is style-src 'self' and would
   drop an inline custom property without a word. Each tone sits inside the
   luminance band 0.135–0.220, which is what clears 3:1 against #1a1a1a and
   #e2e2e2 at the same time.

   Qualified with :root, not bare, so the specificity beats the
   prefers-color-scheme block below — a bare [data-accent] loses to
   :root:not([data-theme]) and the publisher's colour would silently revert for
   every visitor whose OS prefers light. */
:root[data-accent="violet"] { --accent: #7066d0; }
:root[data-accent="vermilion"] { --accent: #e0483a; }
:root[data-accent="ochre"] { --accent: #b85f0a; }
:root[data-accent="olive"] { --accent: #96741b; }
:root[data-accent="green"] { --accent: #2e8f5e; }
:root[data-accent="petrol"] { --accent: #1b838b; }
:root[data-accent="blue"] { --accent: #3f6fe0; }
:root[data-accent="magenta"] { --accent: #d2478e; }

/* The same eight tones as --swatch, for the picker in the backend. The rules
   above only fire on :root, so a swatch carrying data-accent on itself cannot
   read them — it drew all eight in the page's own accent. Same values, one
   file, still no raw colour outside this one. */
[data-accent="violet"] { --swatch: #7066d0; }
[data-accent="vermilion"] { --swatch: #e0483a; }
[data-accent="ochre"] { --swatch: #b85f0a; }
[data-accent="olive"] { --swatch: #96741b; }
[data-accent="green"] { --swatch: #2e8f5e; }
[data-accent="petrol"] { --swatch: #1b838b; }
[data-accent="blue"] { --swatch: #3f6fe0; }
[data-accent="magenta"] { --swatch: #d2478e; }
