/* ═══════════════════════════════════════════════════════════════════════════
   PoTraffic Design Tokens — single source of truth for color / type / spacing
   / elevation / motion. Wired to Radzen's CSS variables so third-party
   components inherit the brand automatically.

   Touch this file alone to reskin the entire app.

   THEMING. Every dual-valued token is declared once with `light-dark()` rather
   than twice in a light block and a dark override. The pair is resolved from
   the element's `color-scheme`, which is set three ways below: unpinned follows
   the OS, and `data-theme` (stamped by the resolver in index.html) pins it. A
   consequence worth having: with no JavaScript at all the palette still follows
   the OS, where the old two-block arrangement fell back to light.

   This file holds TOKENS ONLY. Keyframes and motion utilities live in
   pt-motion.css; component rules live in app.css or a scoped stylesheet.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Colour-scheme resolution ────────────────────────────────────────────────
   Bare :root accepts either, so an unpinned visitor follows the OS. The
   data-theme selectors pin one and are what the in-page resolver writes. */
:root                     { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

:root {
    /* ── Palette (1 primary + 1 neutral + 4 accents) ─────────────────── */
    --pt-color-brand-50:  #eff6ff;
    --pt-color-brand-100: #dbeafe;
    --pt-color-brand-200: #bfdbfe;
    --pt-color-brand-300: #93c5fd;
    --pt-color-brand-400: #60a5fa;
    --pt-color-brand-500: #3b82f6;   /* primary */
    --pt-color-brand-600: #2563eb;
    --pt-color-brand-700: #1d4ed8;
    --pt-color-brand-800: #1e40af;
    --pt-color-brand-900: #1e3a8a;

    --pt-color-neutral-0:   #ffffff;
    --pt-color-neutral-50:  #f8fafc;
    --pt-color-neutral-100: #f1f5f9;
    --pt-color-neutral-200: #e2e8f0;
    --pt-color-neutral-300: #cbd5e1;
    --pt-color-neutral-400: #94a3b8;
    --pt-color-neutral-500: #64748b;
    --pt-color-neutral-600: #475569;
    --pt-color-neutral-700: #334155;
    --pt-color-neutral-800: #1e293b;
    --pt-color-neutral-900: #0f172a;

    /* Accents — semantic. Identical in both themes; the surfaces they sit on
       are what changes, and each accent clears AA on either. */
    /* Success and warning are theme-split. As FILLS on a white card the -500 steps
       measured 2.54:1 and 2.15:1, under the 3:1 that WCAG 1.4.11 asks of a
       graphical object carrying meaning — these paint status dots and heatmap
       cells, so they carry it. One step darker clears it on white; dark mode keeps
       the vivid original, which it needs against a near-black surface.
       Danger and info clear 3:1 on both themes unchanged. */
    --pt-color-success: light-dark(#059669, #10b981);
    --pt-color-warning: light-dark(#d97706, #f59e0b);
    --pt-color-danger:  #f43f5e;
    --pt-color-info:    #6366f1;

    /* Brand at a weight that clears AA on the current surface. Interactive text
       needs a darker blue on white and a lighter one on navy; every link and
       accented label reads this rather than picking a step by hand. */
    --pt-color-brand-on-surface: light-dark(var(--pt-color-brand-700), var(--pt-color-brand-300));

    /* ── Accents, at TEXT weight ───────────────────────────────────────
       The four accents above are chosen to work as FILLS — heatmap cells,
       badges, swipe actions — where a vivid colour is the point. As text they
       are too light: measured against a white card, success came in at 2.54:1
       and warning at 2.15:1, where AA asks 4.5:1.

       These are the same four hues pushed to a weight that clears AA on the
       current surface, exactly as --pt-color-brand-on-surface does. Anything
       that COLOURS TEXT uses these; anything that fills a shape keeps the
       vivid original. */
    --pt-color-success-on-surface: light-dark(#047857, #6ee7b7);
    --pt-color-warning-on-surface: light-dark(#b45309, #fbbf24);
    --pt-color-danger-on-surface:  light-dark(#be123c, #fda4af);
    --pt-color-info-on-surface:    light-dark(#4338ca, #a5b4fc);

    /* ── Surfaces ─────────────────────────────────────────────────────
       Light: page sits lowest, cards are white. Dark: page is darkest and
       cards lift toward the viewer. */
    --pt-bg-page:    light-dark(var(--pt-color-neutral-50), #0b1220);
    --pt-bg-surface: light-dark(var(--pt-color-neutral-0),  #131c2e);
    --pt-bg-elev-1:  light-dark(var(--pt-color-neutral-0),  #131c2e);
    --pt-bg-elev-2:  light-dark(var(--pt-color-neutral-50), #1b2740);
    --pt-bg-sunken:  light-dark(var(--pt-color-neutral-100), #070d18);

    /* ── Foreground ramp — primary → faint ────────────────────────────
       Every text colour in the app resolves through one of these, so no
       component can hard-code a shade that survives into the other theme. */
    --pt-fg-primary:   light-dark(var(--pt-color-neutral-900), var(--pt-color-neutral-50));
    --pt-fg-strong:    light-dark(var(--pt-color-neutral-800), var(--pt-color-neutral-100));
    --pt-fg-body:      light-dark(var(--pt-color-neutral-700), var(--pt-color-neutral-200));
    --pt-fg-secondary: light-dark(var(--pt-color-neutral-600), var(--pt-color-neutral-300));
    /* Not neutral-500: that measured 4.34:1 against the sunken well it is used on
       (captions inside .pt-bg-sunken), just under AA. Nudged darker until the
       WORST surface it appears on clears 4.5, rather than only the best one. */
    --pt-fg-muted:     light-dark(#5a6779, var(--pt-color-neutral-400));
    --pt-fg-tertiary:  light-dark(var(--pt-color-neutral-500), var(--pt-color-neutral-400));
    --pt-fg-faint:     light-dark(var(--pt-color-neutral-400), var(--pt-color-neutral-600));
    --pt-fg-on-brand:  var(--pt-color-neutral-0);

    --pt-border-subtle: light-dark(var(--pt-color-neutral-200), #26334d);
    --pt-border-strong: light-dark(var(--pt-color-neutral-300), var(--pt-color-neutral-600));

    /* Semantic success surface — the "monitoring active" cards. Needs its own
       tokens because a success tint cannot be derived from the neutral ramp. */
    --pt-success-bg-from:   light-dark(#ecfdf5, #052e23);
    --pt-success-bg-to:     light-dark(#d1fae5, #064e3b);
    --pt-success-border:    light-dark(#6ee7b7, #047857);
    --pt-success-fg:        light-dark(#065f46, #6ee7b7);
    --pt-success-fg-strong: light-dark(#064e3b, #a7f3d0);

    /* Subtle tint surfaces (chips, sparkline wells) — an alpha wash over the
       page rather than a fixed grey, washing dark on light and light on dark. */
    --pt-bg-tint:        light-dark(rgb(15 23 42 / 0.07), rgb(255 255 255 / 0.07));
    --pt-bg-tint-strong: light-dark(rgb(15 23 42 / 0.12), rgb(255 255 255 / 0.14));

    /* Semantic danger surface (destructive menu items). */
    --pt-danger-bg: light-dark(#fef2f2, #2a1215);

    /* ── Traffic verdict ramp (route map line) ─────────────────────────
       Green → blue → amber → red, ordered by how much worse than usual the
       latest sample is. Blue, not green, is "normal": green must be reserved
       for the genuinely-better-than-usual case, or a routine commute reads as
       a win. Each pair is lifted for dark so the line stays legible over the
       dark grey of a night-mode map tile. */
    --pt-traffic-clear:   light-dark(#15803d, #4ade80);
    --pt-traffic-normal:  light-dark(#2563eb, #60a5fa);
    --pt-traffic-slow:    light-dark(#b45309, #fbbf24);
    --pt-traffic-heavy:   light-dark(#b91c1c, #f87171);
    --pt-traffic-unknown: light-dark(#64748b, #94a3b8);

    /* ── Ambient wash (the living background) ──────────────────────────
       Two poles the field mixes between, driven by the traffic mood. Kept
       far apart in hue so the shift from "clear" to "jammed" is legible
       peripherally, without ever being read as an alert. */
    --pt-ambient-cool: light-dark(#2166d9, #1d4ed8);
    --pt-ambient-hot:  light-dark(#d9384a, #b91c1c);

    /* Shadow base as raw channels so alpha can vary per use. A slate-tinted
       shadow is invisible on a slate page, so dark goes to pure black.
       Not a <color>, so this one pair cannot use light-dark(); see below. */
    --pt-shadow-rgb: 15, 23, 42;

    /* ── Surface radius (8px scale, used everywhere) ──────────────────── */
    --pt-radius-xs:  4px;
    --pt-radius-sm:  6px;
    --pt-radius-md:  10px;
    --pt-radius-lg:  14px;
    --pt-radius-xl:  20px;
    --pt-radius-full: 9999px;

    /* ── Spacing (8px grid, scaled by density) ─────────────────────────
       --pt-density is 1 by default and re-set by [data-density]; every step
       below multiplies through it, so one attribute on <html> tightens or
       relaxes the whole app. */
    --pt-density: 1;
    --pt-space-0: 0;
    --pt-space-1: calc(0.25rem * var(--pt-density));  /*  4 */
    --pt-space-2: calc(0.5rem  * var(--pt-density));  /*  8 */
    --pt-space-3: calc(0.75rem * var(--pt-density));  /* 12 */
    --pt-space-4: calc(1rem    * var(--pt-density));  /* 16 */
    --pt-space-5: calc(1.25rem * var(--pt-density));  /* 20 */
    --pt-space-6: calc(1.5rem  * var(--pt-density));  /* 24 */
    --pt-space-8: calc(2rem    * var(--pt-density));  /* 32 */
    --pt-space-10: calc(2.5rem * var(--pt-density));  /* 40 */
    --pt-space-12: calc(3rem   * var(--pt-density));  /* 48 */

    /* ── Type scale (modular 1.250, fluid at the top) ──────────────────

       The small end is FIXED. Body copy and captions are read at the same size
       on a phone as on a monitor — scaling them with the viewport makes text
       smaller exactly where the screen is smallest, which is backwards.

       The display sizes are FLUID, via clamp(). A 30px page title that is
       right on a laptop is oversized on a 375px phone and undersized on a
       27-inch monitor; interpolating between 22.5rem (360px) and 90rem
       (1440px) lets one token be correct at both ends.

       No `vw` unit appears outside a clamp() with both bounds set, so the text
       can always be zoomed and never collapses to nothing (WCAG 1.4.4). */
    --pt-text-xs:   0.75rem;     /* 12 */
    --pt-text-sm:   0.875rem;    /* 14 */
    --pt-text-base: 1rem;        /* 16 */
    --pt-text-lg:   1.125rem;    /* 18 */
    --pt-text-xl:   clamp(1.125rem, 1.03rem + 0.42vw, 1.375rem);   /* 18 → 22 */
    --pt-text-2xl:  clamp(1.3rem,   1.11rem + 0.83vw, 1.75rem);    /* 21 → 28 */
    --pt-text-3xl:  clamp(1.55rem,  1.20rem + 1.55vw, 2.375rem);   /* 25 → 38 */

    --pt-leading-tight: 1.25;
    --pt-leading-normal: 1.5;
    --pt-leading-loose: 1.75;

    /* ── Measure (line length) ─────────────────────────────────────────
       Prose past about 75 characters is measurably harder to read: the eye
       loses its place on the return sweep. `ch` is the width of a "0" in the
       current font, so this tracks the type size instead of being a magic
       pixel count. */
    --pt-measure:      68ch;
    --pt-measure-wide: 88ch;

    --pt-weight-regular: 400;
    --pt-weight-medium:  500;
    --pt-weight-semibold: 600;
    --pt-weight-bold:    700;

    /* ── Layout constants ──────────────────────────────────────────────
       The bottom nav's height is read by both the nav itself and the page
       padding that has to clear it. They were independent numbers and had
       drifted apart, so the last rows of every page sat under the nav. */
    --pt-bottom-nav-h: 64px;
    --pt-bottom-nav-safe: calc(var(--pt-bottom-nav-h) + env(safe-area-inset-bottom, 0px));

    /* Minimum hit area for anything tappable (WCAG 2.5.8 asks 24px, Apple and
       Material both ask 44px; we take the larger). */
    --pt-tap-min: 44px;

    /* ── Breakpoints ───────────────────────────────────────────────────
       Custom properties cannot be used inside a media query, so these are
       documentation rather than machinery — but they are the ONLY two widths
       the app is allowed to break at. Anything narrower than sm is a phone;
       anything past md gets the desktop treatment. Component-level reflow
       belongs in a @container query, not here.

         --pt-bp-sm: 640px
         --pt-bp-md: 1024px */

    /* ── Elevation ────────────────────────────────────────────────────── */
    --pt-elev-0: none;
    --pt-elev-1: 0 1px 2px  light-dark(rgb(15 23 42 / 0.06), rgb(0 0 0 / 0.40));
    --pt-elev-2: 0 2px 8px  light-dark(rgb(15 23 42 / 0.08), rgb(0 0 0 / 0.45));
    --pt-elev-3: 0 8px 24px light-dark(rgb(15 23 42 / 0.12), rgb(0 0 0 / 0.50));
    --pt-elev-4: 0 20px 48px light-dark(rgb(15 23 42 / 0.18), rgb(0 0 0 / 0.60));

    /* ── Motion ───────────────────────────────────────────────────────── */
    --pt-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
    --pt-ease-out: cubic-bezier(0, 0, 0.2, 1);
    --pt-dur-instant: 80ms;
    --pt-dur-fast:    150ms;
    --pt-dur-base:    220ms;
    --pt-dur-slow:    320ms;

    /* ── Z-index scale ─────────────────────────────────────────────────── */
    --pt-z-nav:   100;
    --pt-z-pop:   200;
    --pt-z-modal: 300;
    --pt-z-toast: 400;
    --pt-z-debug: 9999;

    /* ── Glass surface tokens ──────────────────────────────────────────
       Dark panes tint from the surface colour rather than from white. */
    --pt-glass-bg:        light-dark(rgb(255 255 255 / 0.72), rgb(19 28 46 / 0.72));
    --pt-glass-bg-strong: light-dark(rgb(255 255 255 / 0.86), rgb(19 28 46 / 0.90));
    --pt-glass-border:    light-dark(rgb(255 255 255 / 0.45), rgb(148 163 184 / 0.18));
    --pt-glass-blur: 16px;
    --pt-glass-saturate: 180%;

    /* ── Radzen theme mapping (so Radzen components inherit brand) ────── */
    --rz-primary:            light-dark(var(--pt-color-brand-600), var(--pt-color-brand-400));
    --rz-primary-light:      var(--pt-color-brand-300);
    --rz-primary-lighter:    light-dark(var(--pt-color-brand-100), var(--pt-color-brand-800));
    --rz-primary-dark:       light-dark(var(--pt-color-brand-700), var(--pt-color-brand-500));
    --rz-primary-darker:     light-dark(var(--pt-color-brand-900), var(--pt-color-brand-600));
    /* Radzen's card, panel and dialog surfaces read --rz-base-background-color,
       NOT --rz-base-100, and it defaults to literal white. Mapping only the base
       ramp left every RadzenCard white on the dark theme while the text inside it
       followed the theme — which is why the sign-in card was unreadable in dark
       mode. These three are the ones that actually paint a surface. */
    --rz-base-background-color:   var(--pt-bg-surface);
    --rz-panel-background-color:  var(--pt-bg-surface);
    --rz-dialog-background-color: var(--pt-bg-surface);
    --rz-text-title-color:        var(--pt-fg-primary);

    /* Radzen's select bar keeps its selected-item colour in its OWN variable, not
       in the primary ramp — and its default is #ff6d41, a burnt orange that
       appears nowhere else in this palette. Left unmapped it painted the chosen
       Theme and Density labels orange on a pale-blue fill. */
    --rz-selectbar-selected-color: var(--pt-fg-on-brand);

    /* The four semantic colours. Only the PRIMARY ramp was mapped above, so every
       Radzen alert, badge and status button — the danger alert on Settings, the
       Healthy/Unhealthy badges on Diagnostics, the quota progress bar — was drawn
       from Radzen's stock palette while the app's own components used --pt-color-*.
       Two greens, two reds and two ambers were in play at once. */
    --rz-success: var(--pt-color-success);
    --rz-danger:  var(--pt-color-danger);
    --rz-warning: var(--pt-color-warning);
    --rz-info:    var(--pt-color-info);

    /* Chart series. The admin line chart is the app's only RadzenChart; pinning the
       first two series keeps it in step with pt-chart.js, which draws series A in
       brand and series B in warning-amber. */
    --rz-series-1: var(--pt-color-brand-500);
    --rz-series-2: var(--pt-color-warning);
    --rz-series-3: var(--pt-color-info);
    --rz-series-4: var(--pt-color-success);

    --rz-base-100:           var(--pt-bg-surface);
    --rz-base-200:           var(--pt-bg-elev-2);
    --rz-base-300:           light-dark(var(--pt-color-neutral-100), var(--pt-color-neutral-700));
    --rz-base-400:           light-dark(var(--pt-color-neutral-200), var(--pt-color-neutral-600));
    --rz-base-500:           light-dark(var(--pt-color-neutral-300), var(--pt-color-neutral-500));
    --rz-text-color:         var(--pt-fg-primary);
    --rz-text-secondary-color: var(--pt-fg-secondary);
    --rz-text-disabled-color: var(--pt-fg-tertiary);
    --rz-border-color:       var(--pt-border-subtle);
    --rz-disabled-opacity:   0.5;
    --rz-corner-radius:      var(--pt-radius-md);
}

/* The one token light-dark() cannot carry: it is a channel list, not a colour.
   Both selectors are needed so it flips for a pinned theme AND for an unpinned
   visitor on a dark OS. */
:root[data-theme="dark"] { --pt-shadow-rgb: 0, 0, 0; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) { --pt-shadow-rgb: 0, 0, 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Density — one attribute on <html>, set from Settings and stamped before
   first paint by the resolver in index.html.
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-density="compact"]     { --pt-density: 0.75; }
:root[data-density="comfortable"] { --pt-density: 1.15; }

/* ═══════════════════════════════════════════════════════════════════════════
   High contrast. The muted end of the ramp is the first thing to fail: at
   default weights --pt-fg-tertiary on a white card is roughly 2.6:1, under AA
   for the small text it labels. Here the ramp collapses toward the primary end
   and the glass goes opaque, because a translucent pane over arbitrary content
   has no contrast guarantee at all.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-contrast: more) {
    :root {
        --pt-fg-secondary: light-dark(var(--pt-color-neutral-900), var(--pt-color-neutral-50));
        --pt-fg-muted:     light-dark(var(--pt-color-neutral-800), var(--pt-color-neutral-100));
        --pt-fg-tertiary:  light-dark(var(--pt-color-neutral-800), var(--pt-color-neutral-100));
        --pt-fg-faint:     light-dark(var(--pt-color-neutral-700), var(--pt-color-neutral-200));

        --pt-border-subtle: light-dark(var(--pt-color-neutral-500), var(--pt-color-neutral-400));
        --pt-border-strong: light-dark(var(--pt-color-neutral-700), var(--pt-color-neutral-200));

        --pt-color-brand-on-surface: light-dark(var(--pt-color-brand-800), var(--pt-color-brand-200));

        --pt-bg-tint:        light-dark(rgb(15 23 42 / 0.14), rgb(255 255 255 / 0.16));
        --pt-bg-tint-strong: light-dark(rgb(15 23 42 / 0.22), rgb(255 255 255 / 0.26));

        --pt-glass-bg:        var(--pt-bg-surface);
        --pt-glass-bg-strong: var(--pt-bg-surface);
        --pt-glass-border:    var(--pt-border-strong);
        --pt-glass-blur: 0px;
        --pt-glass-saturate: 100%;
    }
}

/* Some readers keep full colour but cannot cope with translucency. The app
   leans on backdrop-filter in the status bar, empty states, palette and nav,
   so honour it independently of the contrast preference. */
@media (prefers-reduced-transparency: reduce) {
    :root {
        --pt-glass-bg:        var(--pt-bg-surface);
        --pt-glass-bg-strong: var(--pt-bg-surface);
        --pt-glass-blur: 0px;
        --pt-glass-saturate: 100%;
    }
}
