/* @waev/ui-kit — generated bundle.  DO NOT EDIT.
   Concatenation of every kit sheet wrapped in its canonical @layer.
   Source: packages/ui-kit/scripts/build-bundle.mjs */

@layer ui-kit.reset, ui-kit.base, ui-kit.tokens, ui-kit.themes,
       ui-kit.supports, ui-kit.layout, ui-kit.components,
       ui-kit.responsive, ui-kit.motion, app;

/* ── ui-kit.reset: src/styles/reset.css ── */
@layer ui-kit.reset {
/* ── Reset (modern-normalize-adjacent) ────────────────────────────────────
   Universal box-sizing + body/html/a/img/button/input/strong/code/pre/hr/::selection/:focus-visible. */

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-size: 16px;
  scroll-behavior: smooth;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Single tracking baseline — every text element inherits -1%
     (-0.01em) unless it explicitly overrides letter-spacing.  Pairs
     with the unified --ls-* tokens above so class-based rules that
     bind `letter-spacing: var(--ls-<role>)` resolve to the same
     value and inline styles can fall through to the inherited
     default. */
  letter-spacing: -0.01em;
  /* Prevent overscroll bounce / pull-to-refresh on mobile */
  overscroll-behavior-y: none;
  /* Reserve a stable scrollbar gutter so the page contents do NOT
     shift horizontally when `useScrollLock` removes the scrollbar
     while a modal is open.  Without this, opening a Dialog or
     BottomSheet on Linux/Windows (and on macOS with "Always show
     scrollbars") visibly jumps the body ~15 px to the right.
     Baseline: Safari 16+, Chrome 94+, Firefox 97+ — within the
     existing WebGL2 baseline.  Older browsers ignore the property
     and fall back to the previous behaviour. */
  scrollbar-gutter: stable;
}

body {
  font-family: var(--font);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  /* Body baseline weight — REGULAR (400).  Every text element inherits
     this unless it explicitly overrides.  Headers/header-style classes
     step up to semibold (600); subhead / title classes to medium (500).
     Body copy + reading text ride this 400 baseline so paragraphs read
     light and open (matching the type-standard artifact), instead of the
     old 500 baseline that made all body copy read a weight too heavy.
     Chrome that needs more presence (buttons, nav) sets its own weight. */
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-body-lg);
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
  min-height: 100vh;
  min-height: 100dvh;
  /* Lock vertical axis — no rubber-band bounce */
  overscroll-behavior-y: none;
  -webkit-overflow-scrolling: touch;
}

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

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

button, input, select, textarea {
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
}

strong, b { font-weight: var(--fw-semibold); }

code, kbd {
  font-family: var(--mono);
  font-size: 0.875em;
  letter-spacing: var(--ls-body-md);
}

pre {
  font-family: var(--mono);
  font-size: var(--fs-body-md);
  line-height: var(--lh-body-md);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

hr {
  border: none;
  border-top: 1px solid var(--border-subtle);
  margin-block: var(--sp-6);
}

::selection {
  background: var(--accent);
  color: var(--on-primary);
}

:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}


}

/* ── ui-kit.base: src/styles/base-type.css ── */
@layer ui-kit.base {
/* ── Base type scale (M3 defaults — semantic HTML) ──────────────────────── */

/* ── Base type scale (M3 defaults — semantic HTML) ────────────────────── */
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
p { text-wrap: pretty; }
/* Header roles (h1–h3) all render at semibold; subhead roles (h4–h6)
   at medium.  Class rules that re-skin an h-tag at a different size
   role still inherit these weights unless they explicitly opt out. */
h1 { font-size: var(--fs-headline-lg); line-height: var(--lh-headline-lg); font-weight: var(--fw-semibold); letter-spacing: var(--ls-headline-lg); }
h2 { font-size: var(--fs-headline-md); line-height: var(--lh-headline-md); font-weight: var(--fw-semibold); letter-spacing: var(--ls-headline-md); }
h3 { font-size: var(--fs-headline-sm); line-height: var(--lh-headline-sm); font-weight: var(--fw-semibold); letter-spacing: var(--ls-headline-sm); }
h4 { font-size: var(--fs-title-lg);    line-height: var(--lh-title-lg);    font-weight: var(--fw-medium);   letter-spacing: var(--ls-title-lg); color: var(--text-muted); }
h5 { font-size: var(--fs-title-md);    line-height: var(--lh-title-md);    font-weight: var(--fw-medium);   letter-spacing: var(--ls-title-md); }
h6 { font-size: var(--fs-title-sm);    line-height: var(--lh-title-sm);    font-weight: var(--fw-medium);   letter-spacing: var(--ls-title-sm); }
small { font-size: var(--fs-body-sm);  line-height: var(--lh-body-sm); letter-spacing: var(--ls-body-sm); }

}

/* ── ui-kit.tokens: dist/css/tokens.css ── */
@layer ui-kit.tokens {
/**
 * Do not edit directly, this file was auto-generated.
 */

:root {
  --ind-red: #ff002f;
  --ind-green: #00ff24;
  --ind-yellow: #fff600;
  --ind-blue: #0024ff;
  --ind-indigo: #7700ff;
  --ind-white: #ffffff;
  --sys-zinc-50: #faf9f7;
  --sys-zinc-100: #f4f2f0;
  --sys-zinc-200: #e2dfdc;
  --sys-zinc-250: #d8d5d2;
  --sys-zinc-300: #cfcbc8;
  --sys-zinc-350: #bab7b2;
  --sys-zinc-375: #b1ada9;
  --sys-zinc-400: #a7a29d;
  --sys-zinc-500: #76726d;
  --sys-zinc-600: #54504c;
  --sys-zinc-700: #3f3b37;
  --sys-zinc-800: #24211e;
  --sys-zinc-850: #1b1916;
  --sys-zinc-900: #12100e;
  --sys-zinc-925: #0d0b09;
  --sys-zinc-950: #080605;
  --sys-zinc-975: #030302;
  --sys-white-4: rgba(255, 255, 255, 0.04);
  --sys-white-5: rgba(255, 255, 255, 0.05);
  --sys-white-7: rgba(255, 255, 255, 0.07);
  --sys-white-8: rgba(255, 255, 255, 0.08);
  --sys-white-9: rgba(255, 255, 255, 0.09);
  --sys-white-10: rgba(255, 255, 255, 0.1);
  --sys-white-12: rgba(255, 255, 255, 0.12);
  --sys-white-14: rgba(255, 255, 255, 0.14);
  --sys-white-16: rgba(255, 255, 255, 0.16);
  --sys-white-default: #ffffff;
  --sys-black-4: rgba(0, 0, 0, 0.04);
  --sys-black-6: rgba(0, 0, 0, 0.06);
  --sys-black-8: rgba(0, 0, 0, 0.08);
  --sys-black-12: rgba(0, 0, 0, 0.12);
  --sys-black-40: rgba(0, 0, 0, 0.4);
  --sys-black-50: rgba(0, 0, 0, 0.5);
  --sys-black-60: rgba(0, 0, 0, 0.6);
  --sys-red-50: #d8c0ba;
  --sys-red-100: #d6b0a6;
  --sys-red-200: #d4938c;
  --sys-red-300: #d8665c;
  --sys-red-400: #d4252b;
  --sys-red-500: #bf0030;
  --sys-red-600: #a50009;
  --sys-red-700: #7c192e;
  --sys-red-800: #520c1c;
  --sys-red-900: #291517;
  --sys-red-950: #160b0c;
  --sys-red-500-12: rgba(191, 0, 48, 0.12);
  --sys-orangered-50: #ddb7b2;
  --sys-orangered-100: #dfa199;
  --sys-orangered-200: #e18870;
  --sys-orangered-300: #e35535;
  --sys-orangered-400: #df2014;
  --sys-orangered-500: #c82400;
  --sys-orangered-600: #be1400;
  --sys-orangered-700: #9d1903;
  --sys-orangered-800: #5d1005;
  --sys-orangered-900: #361610;
  --sys-orangered-950: #1d0c08;
  --sys-orangered-500-12: rgba(200, 36, 0, 0.12);
  --sys-redorange-50: #e0bfb5;
  --sys-redorange-100: #e4a69a;
  --sys-redorange-200: #e38d72;
  --sys-redorange-300: #e4643f;
  --sys-redorange-400: #d83c18;
  --sys-redorange-500: #c83700;
  --sys-redorange-600: #ae3400;
  --sys-redorange-700: #a12500;
  --sys-redorange-800: #601805;
  --sys-redorange-900: #3d1408;
  --sys-redorange-950: #210b04;
  --sys-redorange-500-12: rgba(200, 55, 0, 0.12);
  --sys-orange-50: #dec2b4;
  --sys-orange-100: #e0b59c;
  --sys-orange-200: #de9d7f;
  --sys-orange-300: #e68057;
  --sys-orange-400: #e05724;
  --sys-orange-500: #d74105;
  --sys-orange-600: #bf2f02;
  --sys-orange-700: #8f2c19;
  --sys-orange-800: #571b13;
  --sys-orange-900: #381b0f;
  --sys-orange-950: #1e0e08;
  --sys-orange-500-12: rgba(215, 65, 5, 0.12);
  --sys-yelloworange-50: #e4c5ae;
  --sys-yelloworange-100: #e7bb93;
  --sys-yelloworange-200: #e7a774;
  --sys-yelloworange-300: #f09952;
  --sys-yelloworange-400: #ed830d;
  --sys-yelloworange-500: #e47400;
  --sys-yelloworange-600: #d05a00;
  --sys-yelloworange-700: #a34000;
  --sys-yelloworange-800: #5b2709;
  --sys-yelloworange-900: #471d07;
  --sys-yelloworange-950: #270f03;
  --sys-yelloworange-500-12: rgba(228, 116, 0, 0.12);
  --sys-orangeyellow-50: #e2c7aa;
  --sys-orangeyellow-100: #ecc48f;
  --sys-orangeyellow-200: #eab776;
  --sys-orangeyellow-300: #e9ad51;
  --sys-orangeyellow-400: #e6941b;
  --sys-orangeyellow-500: #db8600;
  --sys-orangeyellow-600: #cd7b04;
  --sys-orangeyellow-700: #a86a22;
  --sys-orangeyellow-800: #673c07;
  --sys-orangeyellow-900: #492906;
  --sys-orangeyellow-950: #281603;
  --sys-orangeyellow-500-12: rgba(219, 134, 0, 0.12);
  --sys-yellow-50: #e1d1a7;
  --sys-yellow-100: #e3c990;
  --sys-yellow-200: #e4c05f;
  --sys-yellow-300: #e7b43e;
  --sys-yellow-400: #dea800;
  --sys-yellow-500: #cf9800;
  --sys-yellow-600: #c48900;
  --sys-yellow-700: #a37500;
  --sys-yellow-800: #6c540b;
  --sys-yellow-900: #3c3115;
  --sys-yellow-950: #211a0b;
  --sys-yellow-500-12: rgba(207, 152, 0, 0.12);
  --sys-greenyellow-50: #d4d3a4;
  --sys-greenyellow-100: #ced086;
  --sys-greenyellow-200: #c7c78a;
  --sys-greenyellow-300: #bfbc77;
  --sys-greenyellow-400: #beb942;
  --sys-greenyellow-500: #a8ad1f;
  --sys-greenyellow-600: #a0983c;
  --sys-greenyellow-700: #808315;
  --sys-greenyellow-800: #605b13;
  --sys-greenyellow-900: #38382a;
  --sys-greenyellow-950: #1e1e17;
  --sys-greenyellow-500-12: rgba(168, 173, 31, 0.12);
  --sys-yellowgreen-50: #d1d0a0;
  --sys-yellowgreen-100: #bbc591;
  --sys-yellowgreen-200: #a6b47c;
  --sys-yellowgreen-300: #93a359;
  --sys-yellowgreen-400: #979a4f;
  --sys-yellowgreen-500: #7e8939;
  --sys-yellowgreen-600: #6e7936;
  --sys-yellowgreen-700: #5d6b04;
  --sys-yellowgreen-800: #4c5807;
  --sys-yellowgreen-900: #363823;
  --sys-yellowgreen-950: #1d1e13;
  --sys-yellowgreen-500-12: rgba(126, 137, 57, 0.12);
  --sys-green-50: #c3c8c2;
  --sys-green-100: #a8bfa1;
  --sys-green-200: #92ac85;
  --sys-green-300: #7d9672;
  --sys-green-400: #648a5a;
  --sys-green-500: #547555;
  --sys-green-600: #4d5b40;
  --sys-green-700: #384d26;
  --sys-green-800: #2d3327;
  --sys-green-900: #232923;
  --sys-green-950: #131613;
  --sys-green-500-12: rgba(84, 117, 85, 0.12);
  --sys-bluegreen-50: #bfd3bd;
  --sys-bluegreen-100: #a3c6ad;
  --sys-bluegreen-200: #8eb895;
  --sys-bluegreen-300: #6ea596;
  --sys-bluegreen-400: #5c917f;
  --sys-bluegreen-500: #477f6a;
  --sys-bluegreen-600: #3a6c56;
  --sys-bluegreen-700: #2d614a;
  --sys-bluegreen-800: #253732;
  --sys-bluegreen-900: #1d2c2b;
  --sys-bluegreen-950: #0f1817;
  --sys-bluegreen-500-12: rgba(71, 127, 106, 0.12);
  --sys-greenblue-50: #bed2d0;
  --sys-greenblue-100: #a3c3bd;
  --sys-greenblue-200: #7ea8a7;
  --sys-greenblue-300: #559b9f;
  --sys-greenblue-400: #008da2;
  --sys-greenblue-500: #006d8b;
  --sys-greenblue-600: #005b7e;
  --sys-greenblue-700: #27485d;
  --sys-greenblue-800: #213e4b;
  --sys-greenblue-900: #1c2e3a;
  --sys-greenblue-950: #0f191f;
  --sys-greenblue-500-12: rgba(0, 109, 139, 0.12);
  --sys-blue-50: #b3c7c8;
  --sys-blue-100: #97b7c9;
  --sys-blue-200: #73a0b8;
  --sys-blue-300: #0b76a5;
  --sys-blue-400: #006890;
  --sys-blue-500: #005b8a;
  --sys-blue-600: #004e79;
  --sys-blue-700: #013e6d;
  --sys-blue-800: #052f5e;
  --sys-blue-900: #0e2247;
  --sys-blue-950: #071227;
  --sys-blue-500-12: rgba(0, 91, 138, 0.12);
  --sys-purpleblue-50: #bac1c7;
  --sys-purpleblue-100: #99adc6;
  --sys-purpleblue-200: #8295bc;
  --sys-purpleblue-300: #4564a3;
  --sys-purpleblue-400: #314b91;
  --sys-purpleblue-500: #1c4295;
  --sys-purpleblue-600: #1c3077;
  --sys-purpleblue-700: #212961;
  --sys-purpleblue-800: #152753;
  --sys-purpleblue-900: #172242;
  --sys-purpleblue-950: #0c1224;
  --sys-purpleblue-500-12: rgba(28, 66, 149, 0.12);
  --sys-bluepurple-50: #bcbbc2;
  --sys-bluepurple-100: #9da2c6;
  --sys-bluepurple-200: #8787bf;
  --sys-bluepurple-300: #595fb4;
  --sys-bluepurple-400: #4a559a;
  --sys-bluepurple-500: #3e3e96;
  --sys-bluepurple-600: #2f2d7d;
  --sys-bluepurple-700: #2c2761;
  --sys-bluepurple-800: #1e2152;
  --sys-bluepurple-900: #20223a;
  --sys-bluepurple-950: #11121f;
  --sys-bluepurple-500-8: rgba(62, 62, 150, 0.08);
  --sys-bluepurple-500-12: rgba(62, 62, 150, 0.12);
  --sys-bluepurple-500-15: rgba(62, 62, 150, 0.15);
  --sys-bluepurple-500-20: rgba(62, 62, 150, 0.2);
  --sys-bluepurple-300-8: rgba(89, 95, 180, 0.08);
  --sys-bluepurple-300-12: rgba(89, 95, 180, 0.12);
  --sys-bluepurple-300-15: rgba(89, 95, 180, 0.15);
  --sys-bluepurple-300-20: rgba(89, 95, 180, 0.2);
  --sys-purple-50: #c8bbc8;
  --sys-purple-100: #b1a9ce;
  --sys-purple-200: #a28eb9;
  --sys-purple-300: #8e74a7;
  --sys-purple-400: #714c9e;
  --sys-purple-500: #633893;
  --sys-purple-600: #482e7a;
  --sys-purple-700: #3b285b;
  --sys-purple-800: #2a244a;
  --sys-purple-900: #262231;
  --sys-purple-950: #14121a;
  --sys-purple-500-12: rgba(99, 56, 147, 0.12);
  --sys-redpurple-50: #d7c1d3;
  --sys-redpurple-100: #c5a8c1;
  --sys-redpurple-200: #b387ac;
  --sys-redpurple-300: #a35997;
  --sys-redpurple-400: #983971;
  --sys-redpurple-500: #852962;
  --sys-redpurple-600: #72235e;
  --sys-redpurple-700: #5c1f4b;
  --sys-redpurple-800: #4b1e3b;
  --sys-redpurple-900: #391636;
  --sys-redpurple-950: #1f0c1d;
  --sys-redpurple-500-12: rgba(133, 41, 98, 0.12);
  --sys-purplered-50: #dab7b6;
  --sys-purplered-100: #cca1b4;
  --sys-purplered-200: #d17497;
  --sys-purplered-300: #c6427a;
  --sys-purplered-400: #bc246e;
  --sys-purplered-500: #ac003e;
  --sys-purplered-600: #7f0e34;
  --sys-purplered-700: #5f1d32;
  --sys-purplered-800: #570b2a;
  --sys-purplered-900: #390c21;
  --sys-purplered-950: #1f0612;
  --sys-purplered-500-12: rgba(172, 0, 62, 0.12);
  --font: 'Inter Variable', -apple-system, BlinkMacSystemFont, system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --mono: 'JetBrains Mono Variable', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
  --fw-regular: 300;
  --fw-medium: 400;
  --fw-semibold: 500;
  --fw-bold: 600;
  --fs-display-lg: 3.5625rem;
  --lh-display-lg: 4rem;
  --fs-display-md: 2.8125rem;
  --lh-display-md: 3.25rem;
  --fs-display-sm: 2.25rem;
  --lh-display-sm: 2.75rem;
  --fs-headline-lg: 2rem;
  --lh-headline-lg: 2.5rem;
  --fs-headline-md: 1.75rem;
  --lh-headline-md: 2.25rem;
  --fs-headline-sm: 1.5rem;
  --lh-headline-sm: 2rem;
  --fs-title-lg: 1.375rem;
  --lh-title-lg: 1.75rem;
  --fs-title-md: 1rem;
  --lh-title-md: 1.5rem;
  --fs-title-sm: 0.875rem;
  --lh-title-sm: 1.25rem;
  --fs-body-lg: 1rem;
  --lh-body-lg: 1.5rem;
  --fs-body-md: 0.875rem;
  --lh-body-md: 1.25rem;
  --fs-body-sm: 0.75rem;
  --lh-body-sm: 1rem;
  --fs-label-lg: 0.875rem;
  --lh-label-lg: 1.25rem;
  --fs-label-md: 0.75rem;
  --lh-label-md: 1rem;
  --fs-label-sm: 0.6875rem;
  --lh-label-sm: 1rem;
  --fs-label-xs: 0.625rem;
  --lh-label-xs: 0.875rem;
  --ls-display-lg: -0.01em;
  --ls-display-md: -0.01em;
  --ls-display-sm: -0.01em;
  --ls-headline-lg: -0.01em;
  --ls-headline-md: -0.01em;
  --ls-headline-sm: -0.01em;
  --ls-title-lg: -0.01em;
  --ls-title-md: -0.01em;
  --ls-title-sm: -0.01em;
  --ls-body-lg: -0.01em;
  --ls-body-md: -0.01em;
  --ls-body-sm: -0.01em;
  --ls-label-lg: -0.01em;
  --ls-label-md: -0.01em;
  --ls-label-sm: -0.01em;
  --ls-label-xs: -0.01em;
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --map-ctrl-size: 42.5px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 28px;
  --sp-8: 32px;
  --sp-9: 36px;
  --sp-10: 40px;
  --sp-12: 48px;
  --sp-15: 60px;
  --sp-20: 80px;
  --sp-25: 100px;
  --shape-3xs: 1px;
  --shape-2xs: 4px; /** TEMP flatten (2026-07-07): was 2px, a step in the graduated corner-radius scale. Founder-requested global flatten to a single 4px radius across 2xs–xl while a from-scratch shape pass is pending — hand-correcting every consumer wasn't worth it yet. Revert to the graduated scale once that pass happens. */
  --shape-xs: 4px;
  --shape-sm: 4px; /** TEMP flatten (2026-07-07): was 8px. See `shape-2xs`'s description for the full rationale. */
  --shape-md: 4px; /** TEMP flatten (2026-07-07): was 12px. See `shape-2xs`'s description for the full rationale. */
  --shape-lg: 4px; /** TEMP flatten (2026-07-07): was 16px. See `shape-2xs`'s description for the full rationale. */
  --shape-xl: 4px; /** TEMP flatten (2026-07-07): was 28px. See `shape-2xs`'s description for the full rationale. */
  --shape-full: 9999px;
  --blur-sm: blur(8px);
  --blur-md: blur(10px);
  --blur-lg: blur(12px);
  --blur-xl: blur(16px);
  --blur-2xl: blur(20px);
  --surface-hover: transparent;
  --brand-stable-300: #595fb4;
  --brand-stable-400: #4a559a;
  --maplibre-glass-bg: rgba(0, 0, 0, 0.7);
  --maplibre-glass-border: rgba(255, 255, 255, 0.12);
  --maplibre-glass-fg: rgba(255, 255, 255, 0.8);
  --maplibre-glass-fg-dim: rgba(255, 255, 255, 0.7);
  --maplibre-glass-divider: rgba(255, 255, 255, 0.08);
  --maplibre-glass-hover: rgba(255, 255, 255, 0.08);
  --maplibre-attrib-bg: rgba(0, 0, 0, 0.5);
  --maplibre-attrib-fg: rgba(255, 255, 255, 0.5);
  --grid-columns: 12;
  --grid-body: 1200px;
  --dur-short-2: 100ms;
  --dur-short-3: 200ms;
  --dur-short-4: 300ms;
  --dur-medium-3: 500ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-emphasized-decel: cubic-bezier(0.05, 0.7, 0.1, 1);
  --elevation-1: 0 1px 3px 1px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.3);
  --elevation-2: 0 2px 6px 2px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.3);
  --elevation-3: 0 3px 5px -1px rgba(0,0,0,0.2), 0 6px 10px rgba(0,0,0,0.14), 0 1px 18px rgba(0,0,0,0.12);
  --elevation-4: 0 5px 5px -3px rgba(0,0,0,0.2), 0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12);
  --shadow-elev-inner-glow: rgba(255, 255, 255, 0.18);
  --shadow-elev-occlusion: rgba(0, 0, 0, 0.3);
  --shadow-elev-depth: rgba(0, 0, 0, 0.18);
  --container-bevel-bright: #ababab;
  --container-bevel-mid: #454545;
  --container-stroke: 0.66px;
  --ctl-h-compact: 28px;
  --ctl-h-default: 32px;
  --state-outlined-bg: transparent;
  --z-bg: 0;
  --z-default: 1;
  --z-nav: 10;
  --z-overlay: 50;
  --z-modal: 100;
  --z-toast: 200;
  --sig-0: #f14254;
  --sig-1: #f64f4c;
  --sig-2: #fa5c44;
  --sig-3: #fd693d;
  --sig-4: #ff7c24;
  --sig-5: #ff9000;
  --sig-6: #ffa300;
  --sig-7: #fab700;
  --sig-8: #f1bf00;
  --sig-9: #e6c700;
  --sig-10: #d7d000;
  --sig-11: #c5d900;
  --sig-12: #b1e300;
  --sig-13: #94ed00;
  --sig-14: #64f80a;
  --sig-15: #00ff40;
  --bg: var(--sys-zinc-950);
  --bg-raised: var(--sys-zinc-900);
  --surface: var(--sys-zinc-925);
  --surface-active: var(--sys-white-10);
  --overlay: var(--sys-black-50);
  --text: var(--sys-zinc-100);
  --text-muted: var(--sys-zinc-400);
  --text-tertiary: var(--sys-zinc-400);
  --text-accent: var(--sys-bluepurple-100);
  --tone-bluepurple-fg: var(--sys-bluepurple-100); /** Bluepurple tonal badge foreground — theme-aware step that passes WCAG AA for small text on each theme background. Dark/dim: bluepurple-200; light: bluepurple-600. */
  --tone-blue-fg: var(--sys-blue-200); /** Blue tonal text/icon foreground — WCAG AA-passing step per theme. Dark/dim: blue-200; light: blue-700. */
  --tone-red-fg: var(--sys-red-200); /** Red tonal text/icon foreground — WCAG AA-passing step per theme. Dark/dim: red-300; light: red-700. */
  --tone-purplered-fg: var(--sys-purplered-200); /** Purplered tonal badge foreground — WCAG AA-passing step per theme. Dark/dim: purplered-200; light: purplered-700. */
  --tone-orangered-fg: var(--sys-orangered-300); /** orangered tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-redorange-fg: var(--sys-redorange-300); /** redorange tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-orange-fg: var(--sys-orange-200); /** orange tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-yelloworange-fg: var(--sys-yelloworange-500); /** yelloworange tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-orangeyellow-fg: var(--sys-orangeyellow-600); /** orangeyellow tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-yellow-fg: var(--sys-yellow-600); /** yellow tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-greenyellow-fg: var(--sys-greenyellow-600); /** greenyellow tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-yellowgreen-fg: var(--sys-yellowgreen-500); /** yellowgreen tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-green-fg: var(--sys-green-200); /** green tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-bluegreen-fg: var(--sys-bluegreen-400); /** bluegreen tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-greenblue-fg: var(--sys-greenblue-300); /** greenblue tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-purpleblue-fg: var(--sys-purpleblue-200); /** purpleblue tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-purple-fg: var(--sys-purple-200); /** purple tonal text/icon foreground — WCAG AA-passing step per theme. */
  --tone-redpurple-fg: var(--sys-redpurple-200); /** redpurple tonal text/icon foreground — WCAG AA-passing step per theme. */
  --on-primary: var(--sys-bluepurple-950);
  --on-primary-deep: var(--sys-bluepurple-950);
  --border: var(--sys-zinc-850);
  --border-subtle: var(--sys-white-8);
  --border-hover: var(--sys-white-14);
  --border-focus: var(--sys-bluepurple-400);
  --border-accent: var(--sys-bluepurple-500-20);
  --chart-grid: var(--sys-zinc-850);
  --accent: var(--sys-bluepurple-300);
  --accent-light: var(--sys-bluepurple-400);
  --accent-dark: var(--sys-bluepurple-600);
  --accent-subtle: var(--sys-bluepurple-500-8);
  --glow: var(--sys-bluepurple-500-15);
  --hover-halo: var(--sys-bluepurple-300);
  --badge-success-bg: var(--sys-green-500-12);
  --badge-warning-bg: var(--sys-orangeyellow-500-12);
  --badge-warning-fg: var(--sys-orangeyellow-400);
  --badge-danger-bg: var(--sys-red-500-12);
  --badge-info-bg: var(--sys-greenblue-500-12);
  --surface-sunken: var(--sys-zinc-975);
  --surface-glass: color-mix(in srgb, var(--sys-zinc-900) 75%, transparent);
  --outline-glass: var(--sys-zinc-800);
  --surface-tint-1: var(--sys-white-4);
  --surface-tint-2: var(--sys-white-7);
  --surface-tint-3: var(--sys-white-8);
  --surface-tint-4: var(--sys-white-12);
  --surface-tint-5: var(--sys-white-14);
  --grid-gutter: var(--sp-6);
  --grid-margin: var(--sp-2);
  --pane-spacer: var(--sp-6);
  --pane-padding: var(--sp-4);
  --page-pt: var(--sp-10);
  --page-pb: var(--sp-15);
  --page-px: var(--sp-4);
  --section-gap: var(--sp-12);
  --block-gap: var(--sp-4);
  --card-pad: var(--sp-5);
  --card-pad-tight: var(--sp-4);
  --card-pad-hero: var(--sp-7);
  --card-gap: var(--sp-4);
  --shadow-container: inset 7px 14px 64.1px -46px var(--shadow-elev-inner-glow),
    0 1px 3px 0 var(--shadow-elev-occlusion),
    0 4px 14px -3px var(--shadow-elev-depth);
  --container-bevel: linear-gradient(to bottom right, color-mix(in srgb, var(--container-bevel-bright) 80%, transparent) 0%, color-mix(in srgb, var(--container-bevel-mid) 33%, transparent) 32%, color-mix(in srgb, var(--container-bevel-mid) 66%, transparent) 82%, color-mix(in srgb, var(--container-bevel-bright) 50%, transparent) 100%) border-box;
  --container-border: var(--container-stroke) solid transparent;
  --ctl-pad-inline: var(--sp-4);
  --ctl-pad-inline-compact: var(--sp-3);
  --ctl-fs: var(--fs-label-lg);
  --ctl-lh: var(--lh-label-lg);
  --ctl-ls: var(--ls-label-lg);
  --ctl-fw: var(--fw-medium);
  --ctl-fs-compact: var(--fs-label-md);
  --ctl-shape-pill: var(--shape-sm);
  --ctl-shape-rect: var(--shape-sm);
  --state-filled-bg: var(--brand-stable-300);
  --state-filled-bg-hover: var(--brand-stable-300);
  --state-hover-bg: var(--brand-stable-300);
  --state-tonal-bg: var(--brand-stable-300);
  --state-tonal-bg-hover: var(--brand-stable-300);
  --state-outlined-stroke: var(--brand-stable-400);
  --state-danger-bg: var(--sys-red-500);
  --state-danger-bg-hover: var(--sys-red-600);
  --state-danger-stroke: var(--sys-red-400);
  --ctl-neutral-bg-on-surface: var(--sys-zinc-800);
  --ctl-neutral-bg-on-surface-hover: var(--sys-zinc-800);
  --ctl-neutral-fg: var(--sys-zinc-100);
  --badge-neutral-bg: var(--surface);
  --badge-neutral-fg: var(--text-muted);
  --badge-neutral-stroke: var(--border);
  --badge-accent-bg: var(--accent-subtle);
  --badge-accent-fg: var(--text-accent); /** Bluepurple tonal badge foreground — routes through --text-accent (bluepurple-200 dark/dim, bluepurple-600 light) so .m3-tag--accent passes WCAG AA. */
  --badge-accent-stroke: var(--border-accent);
  --badge-success-fg: var(--tone-green-fg);
  --badge-danger-fg: var(--tone-red-fg);
  --badge-info-fg: var(--tone-greenblue-fg);
  --surface-1: var(--bg-raised);
  --container-bg: linear-gradient(var(--bg), var(--bg)) padding-box,
    var(--container-bevel);
  --container-bg-sunken: linear-gradient(var(--surface-sunken), var(--surface-sunken)) padding-box,
    linear-gradient(-1deg, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.25) 100%) border-box;
  --card-fill-zinc: var(--surface);
  --card-fill-bluepurple-tonal: var(--glow);
  --state-text-bg: var(--surface-tint-1);
  --state-outlined-fg: var(--tone-bluepurple-fg); /** Outlined button/icon label — routes through --tone-bluepurple-fg for WCAG AA text contrast. */
  --state-elevated-bg: var(--bg-raised);
  --state-danger-fg: var(--tone-red-fg);
  --ctl-neutral-bg: var(--bg-raised);
  --ctl-neutral-bg-hover: var(--bg-raised);
}

}

/* ── ui-kit.themes: dist/css/themes/dark.css ── */
@layer ui-kit.themes {
/**
 * Do not edit directly, this file was auto-generated.
 * Theme overrides for data-theme="dark".
 */

:root[data-theme="dark"] {
  --bg: var(--sys-zinc-950);
  --bg-raised: var(--sys-zinc-900);
  --surface: var(--sys-zinc-925);
  --surface-hover: transparent;
  --surface-active: var(--sys-white-10);
  --text: var(--sys-zinc-100);
  --text-muted: var(--sys-zinc-400);
  --text-tertiary: var(--sys-zinc-400);
  --text-accent: var(--sys-bluepurple-100);
  --tone-bluepurple-fg: var(--sys-bluepurple-100);
  --tone-blue-fg: var(--sys-blue-200);
  --tone-red-fg: var(--sys-red-200);
  --tone-purplered-fg: var(--sys-purplered-200);
  --tone-orangered-fg: var(--sys-orangered-300);
  --tone-redorange-fg: var(--sys-redorange-300);
  --tone-orange-fg: var(--sys-orange-200);
  --tone-yelloworange-fg: var(--sys-yelloworange-500);
  --tone-orangeyellow-fg: var(--sys-orangeyellow-600);
  --tone-yellow-fg: var(--sys-yellow-600);
  --tone-greenyellow-fg: var(--sys-greenyellow-600);
  --tone-yellowgreen-fg: var(--sys-yellowgreen-500);
  --tone-green-fg: var(--sys-green-200);
  --tone-bluegreen-fg: var(--sys-bluegreen-400);
  --tone-greenblue-fg: var(--sys-greenblue-300);
  --tone-purpleblue-fg: var(--sys-purpleblue-200);
  --tone-purple-fg: var(--sys-purple-200);
  --tone-redpurple-fg: var(--sys-redpurple-200);
  --border: var(--sys-zinc-850);
  --border-subtle: var(--sys-white-8);
  --border-hover: var(--sys-white-14);
  --border-focus: var(--sys-bluepurple-400);
  --surface-1: var(--sys-zinc-900);
  --surface-sunken: var(--sys-zinc-975);
  --surface-glass: color-mix(in srgb, var(--sys-zinc-900) 75%, transparent);
  --outline-glass: var(--sys-zinc-800);
  --surface-tint-1: var(--sys-white-4);
  --surface-tint-2: var(--sys-white-7);
  --surface-tint-3: var(--sys-white-8);
  --surface-tint-4: var(--sys-white-12);
  --surface-tint-5: var(--sys-white-14);
  --maplibre-glass-bg: rgba(0, 0, 0, 0.70);
  --maplibre-glass-border: rgba(255, 255, 255, 0.12);
  --maplibre-glass-fg: rgba(255, 255, 255, 0.80);
  --maplibre-glass-fg-dim: rgba(255, 255, 255, 0.70);
  --maplibre-glass-divider: rgba(255, 255, 255, 0.08);
  --maplibre-glass-hover: rgba(255, 255, 255, 0.08);
  --maplibre-attrib-bg: rgba(0, 0, 0, 0.50);
  --maplibre-attrib-fg: rgba(255, 255, 255, 0.50);
  --ctl-neutral-bg: var(--bg-raised);
  --ctl-neutral-bg-hover: var(--bg-raised);
  --ctl-neutral-bg-on-surface: var(--sys-zinc-800);
  --ctl-neutral-bg-on-surface-hover: var(--sys-zinc-800);
}

}

/* ── ui-kit.themes: dist/css/themes/dim.css ── */
@layer ui-kit.themes {
/**
 * Do not edit directly, this file was auto-generated.
 * Theme overrides for data-theme="dim".
 */

:root[data-theme="dim"] {
  --chart-grid: var(--sys-zinc-700);
  --bg: var(--sys-zinc-850);
  --bg-raised: #30302F;
  --surface: #262525;
  --surface-hover: transparent;
  --surface-active: var(--sys-white-12);
  --text: var(--sys-zinc-100);
  --text-muted: var(--sys-zinc-300);
  --text-tertiary: var(--sys-zinc-400);
  --text-accent: var(--sys-bluepurple-100);
  --tone-bluepurple-fg: var(--sys-bluepurple-100);
  --tone-blue-fg: var(--sys-blue-200);
  --tone-red-fg: var(--sys-red-200);
  --tone-purplered-fg: var(--sys-purplered-200);
  --tone-orangered-fg: var(--sys-orangered-300);
  --tone-redorange-fg: var(--sys-redorange-300);
  --tone-orange-fg: var(--sys-orange-200);
  --tone-yelloworange-fg: var(--sys-yelloworange-500);
  --tone-orangeyellow-fg: var(--sys-orangeyellow-600);
  --tone-yellow-fg: var(--sys-yellow-600);
  --tone-greenyellow-fg: var(--sys-greenyellow-600);
  --tone-yellowgreen-fg: var(--sys-yellowgreen-500);
  --tone-green-fg: var(--sys-green-200);
  --tone-bluegreen-fg: var(--sys-bluegreen-400);
  --tone-greenblue-fg: var(--sys-greenblue-300);
  --tone-purpleblue-fg: var(--sys-purpleblue-200);
  --tone-purple-fg: var(--sys-purple-200);
  --tone-redpurple-fg: var(--sys-redpurple-200);
  --border: transparent;
  --border-subtle: var(--sys-white-12);
  --border-hover: var(--sys-white-16);
  --border-focus: var(--sys-bluepurple-400);
  --surface-1: #30302F;
  --surface-sunken: #1C1C1B;
  --surface-glass: color-mix(in srgb, #1C1C1B 75%, transparent);
  --outline-glass: var(--sys-zinc-500);
  --surface-tint-1: var(--sys-white-5);
  --surface-tint-2: var(--sys-white-9);
  --surface-tint-3: var(--sys-white-10);
  --surface-tint-4: var(--sys-white-14);
  --surface-tint-5: var(--sys-white-16);
  --maplibre-glass-bg: rgba(28, 28, 27, 0.75);
  --maplibre-glass-border: rgba(255, 255, 255, 0.12);
  --maplibre-glass-fg: rgba(255, 255, 255, 0.80);
  --maplibre-glass-fg-dim: rgba(255, 255, 255, 0.70);
  --maplibre-glass-divider: rgba(255, 255, 255, 0.08);
  --maplibre-glass-hover: rgba(255, 255, 255, 0.08);
  --maplibre-attrib-bg: rgba(28, 28, 27, 0.55);
  --maplibre-attrib-fg: rgba(255, 255, 255, 0.55);
  --shadow-elev-inner-glow: rgba(255, 255, 255, 0.18);
  --shadow-elev-occlusion: rgba(0, 0, 0, 0.20);
  --shadow-elev-depth: rgba(0, 0, 0, 0.12);
  --ctl-neutral-bg: var(--bg-raised);
  --ctl-neutral-bg-hover: var(--bg-raised);
  --ctl-neutral-bg-on-surface: #363535;
  --ctl-neutral-bg-on-surface-hover: #363535;
}

}

/* ── ui-kit.themes: dist/css/themes/light.css ── */
@layer ui-kit.themes {
/**
 * Do not edit directly, this file was auto-generated.
 * Theme overrides for data-theme="light".
 */

:root[data-theme="light"] {
  --bg: var(--sys-zinc-100);
  --bg-raised: var(--sys-zinc-50);
  --surface: var(--sys-zinc-50);
  --surface-hover: transparent;
  --surface-active: var(--sys-black-8);
  --text: var(--sys-zinc-900);
  --text-muted: var(--sys-zinc-600);
  --text-tertiary: var(--sys-zinc-500);
  --text-accent: var(--sys-bluepurple-600);
  --tone-bluepurple-fg: var(--sys-bluepurple-600);
  --tone-blue-fg: var(--sys-blue-700);
  --tone-red-fg: var(--sys-red-700);
  --tone-purplered-fg: var(--sys-purplered-700);
  --tone-orangered-fg: var(--sys-orangered-800);
  --tone-redorange-fg: var(--sys-redorange-800);
  --tone-orange-fg: var(--sys-orange-800);
  --tone-yelloworange-fg: var(--sys-yelloworange-800);
  --tone-orangeyellow-fg: var(--sys-orangeyellow-900);
  --tone-yellow-fg: var(--sys-yellow-900);
  --tone-greenyellow-fg: var(--sys-greenyellow-900);
  --tone-yellowgreen-fg: var(--sys-yellowgreen-900);
  --tone-green-fg: var(--sys-green-800);
  --tone-bluegreen-fg: var(--sys-bluegreen-800);
  --tone-greenblue-fg: var(--sys-greenblue-800);
  --tone-purpleblue-fg: var(--sys-purpleblue-600);
  --tone-purple-fg: var(--sys-purple-600);
  --tone-redpurple-fg: var(--sys-redpurple-700);
  --border: transparent;
  --container-bevel-bright: var(--sys-zinc-50);
  --container-bevel-mid: var(--sys-zinc-300);
  --border-subtle: var(--sys-zinc-250);
  --border-hover: var(--sys-zinc-300);
  --border-focus: var(--sys-bluepurple-500);
  --chart-grid: var(--sys-zinc-200);
  --surface-1: var(--sys-zinc-50);
  --surface-sunken: var(--sys-zinc-200);
  --surface-glass: var(--bg-raised);
  --outline-glass: var(--sys-zinc-250);
  --surface-tint-1: var(--sys-black-4);
  --surface-tint-2: var(--sys-black-6);
  --surface-tint-3: var(--sys-black-8);
  --surface-tint-4: var(--sys-black-12);
  --surface-tint-5: var(--sys-black-12);
  --maplibre-glass-bg: rgba(255, 255, 255, 0.75);
  --maplibre-glass-border: rgba(0, 0, 0, 0.12);
  --maplibre-glass-fg: rgba(0, 0, 0, 0.80);
  --maplibre-glass-fg-dim: rgba(0, 0, 0, 0.65);
  --maplibre-glass-divider: rgba(0, 0, 0, 0.08);
  --maplibre-glass-hover: rgba(0, 0, 0, 0.06);
  --maplibre-attrib-bg: rgba(255, 255, 255, 0.55);
  --maplibre-attrib-fg: rgba(0, 0, 0, 0.55);
  --shadow-elev-inner-glow: rgba(255, 255, 255, 0.06);
  --shadow-elev-occlusion: rgba(0, 0, 0, 0.05);
  --shadow-elev-depth: rgba(0, 0, 0, 0.03);
  --accent: var(--sys-bluepurple-500);
  --hover-halo: rgba(28, 66, 149, 0.30);
  --accent-light: var(--sys-bluepurple-400);
  --on-primary: var(--sys-zinc-100);
  --ctl-neutral-bg: var(--sys-zinc-200);
  --ctl-neutral-bg-hover: var(--sys-zinc-300);
  --ctl-neutral-bg-on-surface: var(--sys-zinc-200);
  --ctl-neutral-bg-on-surface-hover: var(--sys-zinc-300);
  --ctl-neutral-fg: var(--sys-zinc-900);
  --state-elevated-bg: var(--sys-zinc-50);
}

}

/* ── ui-kit.themes: src/styles/theme-extras.css ── */
@layer ui-kit.themes {
/* ── Theme extras: dark card drop-shadow boost + theme-swap suppressor ─── */

/* ── Dark theme — container/surface card depth-shadow boost ────────
   Class-level override: bumps the DEPTH slot (`--shadow-elev-depth`)
   on the canonical container surfaces so cards cast a more pronounced
   separation against the zinc-950 body in explicit dark mode.  The
   liquid-glass base dark depth (0.18) is already heavier than the old
   raised-paper recipe (0.10), so the class-level bump here is softer
   than the previous 0.80 — just enough extra atmosphere to keep the
   card silhouette legible on the near-black body.  The other three
   slots (highlight / reflect / occlusion) inherit the :root anchors
   automatically because `--shadow-container` resolves its
   `var(--shadow-elev-*)` references at use-time on each element.
   Mirrors the dim-theme token-level tune but applied at the class
   level per design spec.  Targets the canonical roster of card
   surfaces (`.m3-card` + every legacy layout alias) via `:is(…)` and
   skips the two flat chrome modifiers (`--flat-inset`, `--flat`) whose
   recipes intentionally drop the shadow stack. */
/* Phase 5f: kit boosts only `.m3-card`.  Legacy app-class aliases that
   participate in the canonical-card chrome opt into the boost via the
   app-side roster (kit-aliases.css). */
:root[data-theme="dark"] .m3-card:not(.m3-card--flat-inset):not(.m3-card--flat) {
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- kit's canonical dark-theme depth-shadow alpha; tokenizing would require a per-theme --shadow-elev-depth-dark fork */
  --shadow-elev-depth: rgba(0, 0, 0, 0.50);
}

/* ── Token extras — sub-tier spacing + icon size + eyebrow tracking ───
   These fill three gaps in the Style-Dictionary-generated ladder that
   the kit consumes from `dist/css/tokens.css`:

   1. Sub-`--sp-1` micro-rhythm (2 px / 6 px) for legend chip gaps,
      truncate padding-block, drag-handle insets, chart Y-label
      breathing room.  Canonical 4-grid jumps from 4 px to 8 px with
      no intermediate; these tokens fill the half-steps.

   2. M3 icon glyph sizing (18 px / 24 px) for icon-fonts whose
      footprint is canonically set via `font-size`.  Previously every
      icon site declared the literal pixel value, so density tuning
      was a 7-call-site sweep.  One token, swap once.

   3. Eyebrow tracking (+0.05em) for uppercase label rows on
      `.metric-meter__label`, `.stat-tile__label`, etc.  Positive
      tracking on small-caps is canonical typographic practice; the
      `--ls-*` ladder ships uniform tight tracking (-0.01em), so the
      eyebrow pattern needs its own slot.

   4. Eyebrow color anchor (--text-eyebrow) for the canonical
      uppercase label rung (PartitionHeading, .metric-meter__label,
      .stat-tile__label).  Derived as a `color-mix` 50% interpolation
      from `--text-muted` toward `--text` in OKLCH so the eyebrow
      gains contrast against the body surface in EVERY theme without
      a per-theme override:
        \u2022 Dark theme  \u2014 zinc-400 \u2192 zinc-100 midpoint \u2248 zinc-250.
          Brighter than the prior `--text-muted` reading; lifts off
          the near-black body so the eyebrow reads as a category
          label, not a quiet ornament.
        \u2022 Dim theme   \u2014 zinc-300 \u2192 zinc-100 midpoint \u2248 zinc-200.
          Same brightening direction as dark.
        \u2022 Light theme \u2014 zinc-700 \u2192 zinc-800 midpoint \u2248 zinc-750.
          DARKER than the prior `--text-muted` reading; the eyebrow
          sinks toward the body-ink anchor instead of dissolving
          into the page background.
      The single `color-mix` formula reverses direction
      automatically because `--text` is itself theme-bound (near-
      white in dark / dim, near-black in light), so the same
      interpolation point produces opposite-direction contrast
      shifts where the operator perceives them as the same intent:
      "give the eyebrow more presence against its body".  Anchored
      at `:root` rather than per-theme so a host element that
      already lives inside an inverted-tone region (a dark card on
      light page, etc.) inherits the right reading without an
      additional cascade rule. */
:root {
  /* Sub-tier spacing \u2014 half-steps on the 4-grid for micro-rhythm. */
  --sp-0_5: 2px;
  --sp-1_5: 6px;

  /* M3 icon glyph sizes \u2014 sized via `font-size` per the Material
     Icons convention.  `sm` = chip / snackbar-close icons; `md` =
     icon button / drawer / rail / text-input trailing icons. */
  --icon-sm: 18px;
  --icon-md: 24px;

  /* Eyebrow letter-spacing \u2014 positive tracking for uppercase label
     rows.  Mirrors the design-system convention that small-caps
     eyebrows read better with looser tracking than the default tight
     `--ls-*` (-0.01em) the kit uses for sentence-cased text. */
  --ls-eyebrow: 0.05em;

  /* Eyebrow color anchor \u2014 50% interpolation between `--text-muted`
     (the prior eyebrow color) and `--text` (the full-contrast anchor).
     See the long block above for the per-theme contrast direction
     this single formula produces.  Consumers (`PartitionHeading`,
     `.metric-meter__label`, `.stat-tile__label`) read this token
     directly so the whole eyebrow family stays coherent across
     every theme. */
  --text-eyebrow: color-mix(in oklch, var(--text-muted), var(--text) 50%);

  /* ── Keyline & stroke tier tokens ──────────────────────────────
     Every box-model ring in the kit picks one of three placement
     tiers (Outer / Mid / Inner) so multiple rings on the same host
     stack PIXEL-PRECISELY on adjacent layers instead of fighting
     each other.  See `Foundations/Keylines & Strokes` in Storybook
     for the full taxonomy + migration table.

       Outer  — sits OUTSIDE the host's border-box; canonical mechanism
                `box-shadow: 0 0 0 <thickness> <colour>` (focus rings,
                hover halos, growing-state effects).
       Mid    — sits AT the host's border-box edge; canonical mechanism
                `border: <thickness> solid <colour>` (the chrome
                keyline itself; the rank-row hover colorization).
       Inner  — sits INSIDE the host's border-box, overlapping the
                content background; canonical mechanism
                `box-shadow: inset 0 0 0 <thickness> <colour>` or a
                `::before` pseudo with padding + mask (gradient
                strokes, animated rings, selection overlays).

     Stacking rule — Outer ring's inner edge = Mid ring's outer edge =
     host's border-box outer edge.  Mid ring's inner edge = Inner
     ring's outer edge = host's padding-box outer edge.  Stacked
     rings therefore stack ADJACENT, not overlapping. */
  --stroke-hairline: 1px;
  --stroke-thick:    2px;

  --keyline-outer: var(--border-focus);
  --keyline-mid:   var(--text-muted);
  --keyline-inner: var(--accent);
}
/* ── <ActiveShimmer> tone defaults per theme ───────────────────────
   The `<ActiveShimmer>` portable React component renders a comet as
   sixteen stacked 1 px strokes at 50 % stroke-opacity — same path
   geometry, different dash lengths, all centred on the same point so
   the brightest / shortest stroke paints on top and the dimmer /
   longer strokes fade off-centre for a "fade center out" gradient.
   Stroke alpha composites multiplicatively, so 16 semi-transparent
   layers stack to nearly full opacity at the centre while a single
   layer at the tips reads at 50 %.

   The sixteen steps walk UP the indigo `--sys-bluepurple-*` ramp from
   `step-1` (outermost, dim) to `step-16` (innermost, bright).  Every
   4th step sits on a native ramp stop; the three steps between each
   pair of native stops are `color-mix(in oklch, A, B P%)` interpolations
   at 25 / 50 / 75 % from one stop to the next, giving perceptually-
   uniform quarter-step resolution along the ramp.
   `--active-shimmer-trough` is still used for the dim continuous
   outline track that paints between comet passes.

   Theme translation: each tier above dark shifts every step UP the
   ramp by one brightness stop so contrast against the host surface
   stays comparable on every theme without per-theme props.  Host-
   level overrides on any step variable still win (custom-property
   cascade), so a danger-toned card can pin its own red ramp regardless
   of the active theme. */
:root[data-theme="dark"] {
  --active-shimmer-trough: var(--sys-bluepurple-600);
  /* Outer-glow tonal anchors — one indigo step brighter / darker than
     the trough.  Used by the glow keyframe animation to cycle through
     three indigo shades across the breathing cycle. */
  --active-shimmer-glow-bright: var(--sys-bluepurple-500);
  --active-shimmer-glow-dark:   var(--sys-bluepurple-700);
  /* Two new low-end stops sit between the trough (600) and the
     original brightest-stop-of-the-low-octave (500), interpolated in
     OKLCH at 1/3 and 2/3.  Adds dim shoulder to the centre-out fade. */
  --active-shimmer-step-1: color-mix(in oklch, var(--sys-bluepurple-600), var(--sys-bluepurple-500) 33%);
  --active-shimmer-step-2: color-mix(in oklch, var(--sys-bluepurple-600), var(--sys-bluepurple-500) 67%);
  --active-shimmer-step-3: var(--sys-bluepurple-500);
  --active-shimmer-step-4: color-mix(in oklch, var(--sys-bluepurple-500), var(--sys-bluepurple-400) 25%);
  --active-shimmer-step-5: color-mix(in oklch, var(--sys-bluepurple-500), var(--sys-bluepurple-400) 50%);
  --active-shimmer-step-6: color-mix(in oklch, var(--sys-bluepurple-500), var(--sys-bluepurple-400) 75%);
  --active-shimmer-step-7: var(--sys-bluepurple-400);
  --active-shimmer-step-8: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 25%);
  --active-shimmer-step-9: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 50%);
  --active-shimmer-step-10: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 75%);
  --active-shimmer-step-11: var(--sys-bluepurple-300);
  --active-shimmer-step-12: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 25%);
  --active-shimmer-step-13: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 50%);
  --active-shimmer-step-14: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 75%);
  --active-shimmer-step-15: var(--sys-bluepurple-200);
  --active-shimmer-step-16: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 25%);
  --active-shimmer-step-17: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 50%);
  --active-shimmer-step-18: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 75%);
}
:root[data-theme="dim"] {
  --active-shimmer-trough: var(--sys-bluepurple-500);
  --active-shimmer-glow-bright: var(--sys-bluepurple-400);
  --active-shimmer-glow-dark:   var(--sys-bluepurple-600);
  --active-shimmer-step-1: color-mix(in oklch, var(--sys-bluepurple-500), var(--sys-bluepurple-400) 33%);
  --active-shimmer-step-2: color-mix(in oklch, var(--sys-bluepurple-500), var(--sys-bluepurple-400) 67%);
  --active-shimmer-step-3: var(--sys-bluepurple-400);
  --active-shimmer-step-4: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 25%);
  --active-shimmer-step-5: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 50%);
  --active-shimmer-step-6: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 75%);
  --active-shimmer-step-7: var(--sys-bluepurple-300);
  --active-shimmer-step-8: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 25%);
  --active-shimmer-step-9: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 50%);
  --active-shimmer-step-10: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 75%);
  --active-shimmer-step-11: var(--sys-bluepurple-200);
  --active-shimmer-step-12: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 25%);
  --active-shimmer-step-13: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 50%);
  --active-shimmer-step-14: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 75%);
  --active-shimmer-step-15: var(--sys-bluepurple-100);
  --active-shimmer-step-16: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 25%);
  --active-shimmer-step-17: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 50%);
  --active-shimmer-step-18: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 75%);
}
:root[data-theme="light"] {
  --active-shimmer-trough: var(--sys-bluepurple-400);
  --active-shimmer-glow-bright: var(--sys-bluepurple-300);
  --active-shimmer-glow-dark:   var(--sys-bluepurple-500);
  --active-shimmer-step-1: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 33%);
  --active-shimmer-step-2: color-mix(in oklch, var(--sys-bluepurple-400), var(--sys-bluepurple-300) 67%);
  --active-shimmer-step-3: var(--sys-bluepurple-300);
  --active-shimmer-step-4: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 25%);
  --active-shimmer-step-5: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 50%);
  --active-shimmer-step-6: color-mix(in oklch, var(--sys-bluepurple-300), var(--sys-bluepurple-200) 75%);
  --active-shimmer-step-7: var(--sys-bluepurple-200);
  --active-shimmer-step-8: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 25%);
  --active-shimmer-step-9: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 50%);
  --active-shimmer-step-10: color-mix(in oklch, var(--sys-bluepurple-200), var(--sys-bluepurple-100) 75%);
  --active-shimmer-step-11: var(--sys-bluepurple-100);
  --active-shimmer-step-12: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 25%);
  --active-shimmer-step-13: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 50%);
  --active-shimmer-step-14: color-mix(in oklch, var(--sys-bluepurple-100), var(--sys-bluepurple-50) 75%);
  --active-shimmer-step-15: var(--sys-bluepurple-50);
  --active-shimmer-step-16: color-mix(in oklch, var(--sys-bluepurple-50), white 25%);
  --active-shimmer-step-17: color-mix(in oklch, var(--sys-bluepurple-50), white 50%);
  --active-shimmer-step-18: color-mix(in oklch, var(--sys-bluepurple-50), white 75%);
}

/* ── <ActiveShimmer> host wrappers ─────────────────────────────
   The `.active-shimmer-host*` class family has been replaced by the
   CSS-native `.active-shimmer*` family declared in
   `styles/components/active-shimmer.css`.  The new approach mutates
   the host element in place (via `::before` pseudo + conic-gradient +
   mask-composite) rather than wrapping content for an SVG overlay.
   See `active-shimmer.css` for the implementation. */

/* ── Theme-swap transition suppressor ────────────────────────
   `applyTheme()` in `lib/theme.ts` adds `theme-swapping` to <html>
   for one rAF cycle around the `data-theme` attribute change.  While
   the class is present, every element's `transition` is forced to
   `none` so chrome colours flip in-sync rather than each tweening on
   its own duration (100–300 ms across the system) and producing a
   visible "wave" of mismatched colours.  The class is removed on the
   next frame so hover / focus animations resume normally.

   Targets `*` plus `::before` / `::after` because pseudo-elements
   (drag handles, ribbon underlines, etc.) also carry transitions. */
.theme-swapping,
.theme-swapping *,
.theme-swapping *::before,
.theme-swapping *::after {
  transition: none !important;
}


}

/* ── ui-kit.supports: src/styles/supports.css ── */
@layer ui-kit.supports {
/* ── P3 + color-mix @supports overrides (kit-canonical theme add-ons) ───── */

/* ── P3 wide-gamut palette overrides ───────────────────────────────
   The wholesale Prismatic wheel transcreation pulls every
   chromatic hex straight from the designer's reference SVG.  Those
   180 cells are the canonical source of truth — nudging them with
   a generic +20 % OKLCH C bump on wide-gamut hardware would distort
   the curated wheel.  Only the pure-indicator P3 primaries (--ind-*)
   survive the P3 override layer; every chromatic family (including
   the wheel's BLUE-PURPLE sector, which is the new accent anchor)
   inherits its sRGB hex on every display class. */
@supports (color: color(display-p3 0 0 0)) {
  @media (color-gamut: p3) {
    :root {
      /* Indicators — P3-max with wheel-aligned hue twist.  Same
         lightness + chroma as the legacy pure-P3 primaries, just
         rotated a few degrees toward the wheel's RED / YELLOW /
         GREEN sectors so the status family reads as one piece with
         the wheel-aligned chrome.  See the sRGB fallback block above
         for the per-channel rationale.

         `--ind-blue` pushes P3 primary blue (B=1) with G ≈ 0.140
         (no red), MATCHING `--ind-green`'s 0.140 blue trace so the
         two siblings carry identical saturation depth.  Same primary-
         channel-at-gamut-max + secondary-trace-at-0.140 recipe across
         the whole indicator family; the chosen trace is the smallest
         that still nudges the hue toward the wheel BLUE sector
         (lands around H ≈ 260° — still clearly blue-identity, no
         longer reaching all the way to the wheel anchor at 240°)
         in exchange for full saturation + brightness parity with the
         rest of the indicator family.

         `--ind-indigo` pushes P3 primary blue (B=1) with R ≈ 0.467
         (no green) so the hue twists past the wheel BLUE-PURPLE
         sector toward the visible-violet edge that approximates
         ultraviolet (H ≈ 291°, ~400 nm spectral cutoff).  Maximum
         brightness + maximum saturation at the P3 gamut edge for
         that hue — the "UV indicator" sibling of the red/green/
         yellow status family. */
      --ind-red:           color(display-p3 1 0     0.183);  /* H ≈ 24°  */
      --ind-green:         color(display-p3 0 1     0.140);  /* H ≈ 144° */
      --ind-yellow:        color(display-p3 1 0.963 0);      /* H ≈ 100° */
      --ind-blue:          color(display-p3 0 0.140 1);      /* H ≈ 260° — max sat, trace=ind-green */
      --ind-indigo:        color(display-p3 0.467 0 1);      /* H ≈ 291° — UV approx */
      --ind-white:         color(display-p3 1 1     1);      /* gamut-max white — cursor pointer orphan */
    }
  }
}

/* ── color-mix() upgrade — dynamic palette-linked transparency ──────────
   Overrides the static rgba fallbacks above with color-mix() versions
   so the alpha-blended vars track palette retunes on capable displays.
   18 entries cover the wheel families; the previous legacy aliases
   (amber/lime/emerald/teal/cyan/sky/violet/fuchsia/pink/rose) have been
   retired entirely — the find-replace migrated every consumer to its
   canonical wheel sector, so no alias-inheritance layer is needed.
   Bluepurple carries an extra alpha ladder (-8 / -15 / -20) because it
   anchors the semantic accent tokens (--accent-subtle / --glow /
   --border-accent) and needs a wider range of alpha siblings than the
   wheel default. */
@supports (color: color-mix(in srgb, red 50%, blue)) {
  :root {
    /* Accent (wheel blue-purple sector) — extra alpha ladder. */
    --sys-bluepurple-500-8:  color-mix(in srgb, var(--sys-bluepurple-500)  8%, transparent);
    --sys-bluepurple-500-15: color-mix(in srgb, var(--sys-bluepurple-500) 15%, transparent);
    --sys-bluepurple-500-20: color-mix(in srgb, var(--sys-bluepurple-500) 20%, transparent);
    /* Wheel — 18 sectors clockwise from RED at 12 o'clock */
    --sys-red-500-12:          color-mix(in srgb, var(--sys-red-500)          12%, transparent);
    --sys-orangered-500-12:    color-mix(in srgb, var(--sys-orangered-500)    12%, transparent);
    --sys-redorange-500-12:    color-mix(in srgb, var(--sys-redorange-500)    12%, transparent);
    --sys-orange-500-12:       color-mix(in srgb, var(--sys-orange-500)       12%, transparent);
    --sys-yelloworange-500-12: color-mix(in srgb, var(--sys-yelloworange-500) 12%, transparent);
    --sys-orangeyellow-500-12: color-mix(in srgb, var(--sys-orangeyellow-500) 12%, transparent);
    --sys-yellow-500-12:       color-mix(in srgb, var(--sys-yellow-500)       12%, transparent);
    --sys-greenyellow-500-12:  color-mix(in srgb, var(--sys-greenyellow-500)  12%, transparent);
    --sys-yellowgreen-500-12:  color-mix(in srgb, var(--sys-yellowgreen-500)  12%, transparent);
    --sys-green-500-12:        color-mix(in srgb, var(--sys-green-500)        12%, transparent);
    --sys-bluegreen-500-12:    color-mix(in srgb, var(--sys-bluegreen-500)    12%, transparent);
    --sys-greenblue-500-12:    color-mix(in srgb, var(--sys-greenblue-500)    12%, transparent);
    --sys-blue-500-12:         color-mix(in srgb, var(--sys-blue-500)         12%, transparent);
    --sys-purpleblue-500-12:   color-mix(in srgb, var(--sys-purpleblue-500)   12%, transparent);
    --sys-bluepurple-500-12:   color-mix(in srgb, var(--sys-bluepurple-500)   12%, transparent);
    --sys-purple-500-12:       color-mix(in srgb, var(--sys-purple-500)       12%, transparent);
    --sys-redpurple-500-12:    color-mix(in srgb, var(--sys-redpurple-500)    12%, transparent);
    --sys-purplered-500-12:    color-mix(in srgb, var(--sys-purplered-500)    12%, transparent);
  }
}


}

/* ── ui-kit.layout: src/styles/layout.css ── */
@layer ui-kit.layout {
/* ── Layout composition (M3 pane model + grid + Flutter-port primitives) ── */

/* ── Layout composition (M3 pane model + grid) ──────────────────── */
.layout-container {
  width: calc(100% - 2 * var(--grid-margin));
  max-width: calc(var(--grid-body) - 2 * var(--grid-margin));
  margin-inline: auto;
  overflow: clip;  /* both axes — avoids spec-forced overflow-y:hidden pairing */
}

.layout-grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gutter);
}

.pane-row    { display: flex; gap: var(--grid-gutter); container-type: inline-size; min-width: 0; }
.pane-stack  { display: flex; flex-direction: column; gap: var(--pane-spacer); min-width: 0; }
.pane        { min-width: 0; overflow: clip; }    /* flex child + containment boundary */
.pane--flex  { flex: 1; }         /* fluid pane */
.pane--fixed { flex: none; width: calc(var(--pane-span, var(--grid-columns)) * (100% + var(--grid-gutter)) / var(--grid-columns) - var(--grid-gutter)); }

/* ── Single-child layout widgets ──────────────────────────────────────── */
.layout-align           { display: flex; flex: 1; }
.layout-aspect-ratio    { overflow: hidden; }
.layout-center          { display: flex; align-items: center; justify-content: center; flex: 1; }
.layout-constrained     { box-sizing: border-box; }
.layout-expanded        { /* flex set inline */ }
.layout-fitted          { overflow: hidden; position: relative; }
.layout-fitted__inner   { width: 100%; height: 100%; }
.layout-fractional      { box-sizing: border-box; }
.layout-intrinsic-h     { display: flex; flex-direction: row; align-items: stretch; }
.layout-intrinsic-w     { width: fit-content; }
.layout-limited         { box-sizing: border-box; }
.layout-offstage        { /* visible by default */ }
.layout-offstage--hidden{ visibility: hidden; position: absolute; pointer-events: none; }
.layout-overflow        { overflow: visible; }
.layout-padding         { box-sizing: border-box; }
.layout-sizedbox        { box-sizing: border-box; flex-shrink: 0; }
.layout-transform       { /* transform set inline */ }

/* ── Multi-child layout widgets ───────────────────────────────────────── */
.layout-row             { display: flex; flex-direction: row; min-width: 0; }
.layout-column          { display: flex; flex-direction: column; min-width: 0; }
.layout-stack           { display: grid; position: relative; min-width: 0; }
.layout-stack > *       { grid-area: 1 / 1; }  /* all children in same cell */
.layout-positioned      { position: absolute; z-index: 1; }
.layout-wrap            { display: flex; flex-wrap: wrap; min-width: 0; }
.layout-gridview        { display: grid; min-width: 0; }
.layout-listview        { display: flex; min-height: 0; min-width: 0; }
.layout-listview--divider > * + * {
  border-top: 1px solid var(--border-subtle);
  padding-top: var(--sp-2);
}
.layout-indexed-stack               { display: grid; min-width: 0; }
.layout-indexed-stack__child         { grid-area: 1 / 1; }
.layout-builder         { /* ResizeObserver wrapper */ }
.layout-table           { display: grid; min-width: 0; }
.layout-table-row       { display: contents; }
.layout-table-cell      { padding: var(--sp-2) var(--sp-3); }
.layout-table-cell--header {
  font-weight: var(--fw-semibold);
  font-size: var(--fs-label-sm);
  letter-spacing: var(--ls-label-sm);
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-subtle);
}

/* ── TableCell orthogonal modifiers ─────────────────────────────
   `mono` / `numeric` / `strong` / `align` compose freely.  Reach for
   these props instead of inline `style={{ fontFamily, textAlign,
   fontWeight }}` chains on every cell of a typed column — the prop
   surface makes the `Foundations/Density § row-mate height contract`
   enforceable at the call site rather than at every inline style. */
.layout-table-cell--mono {
  font-family: var(--mono);
}
.layout-table-cell--numeric {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.layout-table-cell--strong {
  font-weight: var(--fw-semibold);
}
.layout-table-cell--align-start  { text-align: start; }
.layout-table-cell--align-end    { text-align: end; }
.layout-table-cell--align-center { text-align: center; }

/* ── TableRow totals variant ────────────────────────────────
   Semantic footer row that summarises the columns above.  Paints a
   1 px top stroke against the kit's canonical `--border-subtle` divider
   and bumps every child cell to `--fw-semibold`.  Composes with the
   per-cell `mono` / `numeric` props — totals rows are typically a
   row of numeric cells where the last header cell carries the row's
   label ("Total"). */
.layout-table-row--totals > .layout-table-cell {
  border-top: 1px solid var(--border-subtle);
  font-weight: var(--fw-semibold);
  background: var(--surface-tint-1);
}
.layout-carousel {
  display: flex;
  overflow-x: auto;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  min-width: 0;
}
.layout-carousel::-webkit-scrollbar { display: none; }
.layout-carousel > * { flex-shrink: 0; scroll-snap-align: start; }

/* ── Scroll composition widgets ──────────────────────────────────────── */
.layout-scrollview      { -webkit-overflow-scrolling: touch; }
.layout-sticky-header   { background: var(--bg); }
.layout-sticky-sentinel { height: 0; }
.layout-sliver-list     { overflow-y: auto; -webkit-overflow-scrolling: touch; }
.layout-sliver-grid     { overflow-y: auto; -webkit-overflow-scrolling: touch; }
.layout-sliver-padding  { box-sizing: border-box; }
.layout-fill-remaining  { flex: 1; min-height: 0; }


}

/* ── ui-kit.components: src/styles/components/m3-controls.css ── */
@layer ui-kit.components {
/* ── M3 controls: container-cascade + button + icon-btn + fab + segmented + badge + tag + progress + indicator + snackbar ─ */

/* ── Container-surface neutral-fill cascade ───────────────────────────
   Any element painting `--container-bg` (cards, feature tiles,
   disclosures, the live-packets card, etc.) re-points the neutral
   control fill to its `-on-surface` sibling so a button NESTED inside
   the container automatically reads at the correct elevation step
   above the container fill, in every theme.  See `:root` block above
   for the per-theme values of `--ctl-neutral-bg-on-surface`.

   IMPORTANT: this 9-class roster is paired with the canonical card
   chrome roster under "Unified Card System" below — the chrome painter
   declares the bevel/radius/shadow for the same 8 base selectors
   (without `.m3-card--elevated`, which sits outside the canonical chrome
   family per its own block below).  Both rosters MUST stay in sync
   when adding a new card-like container; reach for `.m3-card` and the
   canonical chrome modifiers before inventing a new alias. */
/* Phase 5f: kit owns only the `.m3-card` + `.m3-card--elevated` entries
   in the neutral-fill cascade.  Legacy app-class aliases
   (`.feature-card`, `.pkt-card`, `.edge-shelf-disclosure`,
   `.connect-card`, `.connect-section`, `.net-rank-row`,
   `.nd-status-card`) opt in via the app-side roster in
   `frontend/src/styles/kit-aliases.css`. */
.m3-card,
.m3-card--elevated {
  --ctl-neutral-bg:        var(--ctl-neutral-bg-on-surface);
  --ctl-neutral-bg-hover:  var(--ctl-neutral-bg-on-surface-hover);
}

/* ── M3 Material Components

/* Button */
.m3-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--ctl-h-default);
  min-width: 48px;
  padding-inline: var(--sp-6);
  border: none;
  border-radius: var(--ctl-shape-pill);
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  font-weight: var(--ctl-fw);
  letter-spacing: var(--ctl-ls);
  font-family: var(--font);
  cursor: pointer;
  transition: background var(--dur-short-3) var(--ease-standard),
              color var(--dur-short-3) var(--ease-standard),
              box-shadow var(--dur-short-3) var(--ease-standard),
              border-color var(--dur-short-3) var(--ease-standard);
  user-select: none;
  white-space: nowrap;
  text-decoration: none;
}
/* Disabled state — single source of truth for every M3 control variant.
   Strips the gradient bevel outline (`::after`, painted by the
   "Gradient bevel outline" block below for rounded controls) and
   repaints the fill on the canonical `--card-fill-zinc` neutral so
   every disabled variant collapses to ONE quiet treatment regardless
   of which fill the active variant carried.  Opacity 0.38 reduces
   overall presence per M3 disabled spec.  See the matching
   `:disabled::after { content: none; }` block under the gradient
   bevel rules below for the outline-toggle half of the contract. */
.m3-btn:disabled        { opacity: 0.38; cursor: default; pointer-events: none; background: var(--card-fill-zinc); color: var(--text-muted); border: none; }
.m3-btn__icon           { width: 18px; height: 18px; flex-shrink: 0; display: flex; align-items: center; }
.m3-btn--has-icon       { padding-inline-start: var(--sp-4); }
/* Compact size — 32 dp height matching the Select / Chip / TextField /
   SegmentedButton compact family.  Opt in via the `compact` prop on
   <LiveButton> (or any future compact <Button> consumer). */
.m3-btn--compact        { height: var(--ctl-h-compact); padding-inline: var(--ctl-pad-inline-compact); font-size: var(--ctl-fs-compact); }
/* Variants — each reads from the canonical `--state-*` tokens (`:root`).
   Filled / tonal anchor on the dominant bluepurple-300 hex (`#595fb4`)
   so the saturated baseline reads as one highlight tone.  Outlined is
   CANONICAL M3: transparent fill + 2 px solid stroke + matching label,
   both anchored on the theme-stable bluepurple-400 hex (`#4a559a`).
   Filled fg follows `--on-primary` (per-mode pivot — deep -950 in
   dark/dim, BRIGHT -50 in light); text / tonal / elevated rest fg =
   bluepurple-950 (theme-stable deep glyph per design spec).  Per design
   spec rest = hover (no per-variant hover differentiation); the global
   :hover rule paints the same -200 lift. */
.m3-btn--filled         { background: var(--state-filled-bg); color: var(--sys-zinc-100); }
/* Canonical M3 outlined — transparent fill, 2 px solid stroke + matching
   label color, both anchored on the theme-stable bluepurple-400 hex
   (`#4a559a`) so the outline paints the SAME stroke across dark / dim /
   light.  The light-mode palette pivot would otherwise remap -400 → -700
   for legibility; literal hex bypasses that pivot per design spec.
   Stroke bumped from 1 px to 2 px per design spec — the heavier ring
   gives the outlined variant more visual weight against the bluepurple-
   300 fill/hover roster so it reads as a peer rather than a hairline.
   Label bumped one weight step from `--ctl-fw` (medium / 500) to
   `--fw-semibold` (600) per design spec so the outlined label reads
   as a peer of the heavier ring rather than a hairline-weight inset.
   Bevel gradient `::after` suppressed (see the rounded-controls bevel
   block) so the visible ring is the clean 2 px stroke alone. */
.m3-btn--outlined       { background: var(--state-outlined-bg); color: var(--state-outlined-fg); border: 2px solid var(--state-outlined-stroke); font-weight: var(--fw-semibold); }
.m3-btn--outlined:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
/* Text variant — the lowest-emphasis treatment.  Bg = `--surface-tint-1`
   (canonical faint hover wash, theme-aware via the surface-tint scale)
   so the chip reads as a soft padded label against the page chrome.
   Color = `--sys-bluepurple-300` (BRIGHT bluepurple-300 / `#595fb4` in
   dark / dim, palette-pivoted to bluepurple-800 / `#1e2152` in light
   for legibility on the paper body).  Replaces the previous deep -950
   anchor that washed out against dark surfaces. */
/* `--text-accent` = bluepurple-200 (dark/dim) / bluepurple-600 (light): passes WCAG AA for button label text. */
.m3-btn--text           { background: var(--state-text-bg); color: var(--text-accent); padding-inline: var(--sp-3); }
/* Elevated variant — lifted neutral surface.  Bg = `--state-elevated-bg`
   (`--bg-raised` in dark / dim, zinc-300 in light per the light-mode
   override block above).  Color = `--sys-bluepurple-300` (bright -300
   in dark / dim, palette-pivoted to deep -800 in light) so the elevated
   label reads cleanly on either raised surface treatment. */
.m3-btn--elevated       { background: var(--state-elevated-bg); color: var(--text-accent); box-shadow: var(--elevation-2); }
/* Tonal variant — derived from the bluepurple-300 hex (alpha 15% via
   `--state-tonal-bg = --sys-bluepurple-300-15`) so tonal sits on the
   SAME chromatic anchor as filled / outlined / hover, just at a
   translucent step.  Color = `--sys-bluepurple-300` so the label reads
   as the dominant accent tone (bright in dark / dim, palette-pivoted
   to -800 in light for legibility on the paper-wash bg). */
.m3-btn--tonal          { background: var(--state-tonal-bg); color: var(--sys-zinc-100); }
/* Neutral variant — surface-aware fill that picks up the parent
   container colour (or one elevation step above when nested inside a
   card / panel / disclosure).  See the `--ctl-neutral-bg` definition
   in `:root` and the container cascade rule above.  Promoted rest
   matches the previous hover step (one notch up the contrast ladder).
   Color = zinc-100 (theme-stable bright glyph) so the neutral chip
   reads as a quiet bright-on-soft pair. */
.m3-btn--neutral        { background: var(--ctl-neutral-bg-hover); color: var(--ctl-neutral-fg); }

/* ── Danger tone (orthogonal modifier) ───────────────────────────────
   Compose with `.m3-btn--{filled,outlined,text}` for the canonical
   destructive-action chrome on each variant family.  Anchors on the
   wheel RED sector (`--state-danger-bg = sys-red-500` for the saturated
   fill, `--state-danger-fg/-stroke = sys-red-400` for the outlined +
   text label) and mirrors the canonical bluepurple control contract
   (filled-bg + zinc-100 glyph, outlined-stroke + matching label).

   Chained-class selectors (`.m3-btn--tone-danger.m3-btn--filled`)
   give each rule (0,2,0) specificity — ties the base variant rule's
   (0,1,0) and wins via source order.  Hover state painted in
   `contracts.css` AFTER the global hover painter (which would
   otherwise repaint these as bluepurple-300).

   API: emit via the `tone="danger"` prop on `<Button>` / `<IconButton>`.
   The class lands ALONGSIDE the variant modifier so existing variant
   chrome (border radius, padding, font, gradient bevel) flows through
   unchanged — only the color + bg tokens flip to the red anchor.    */
.m3-btn--tone-danger.m3-btn--filled   { background: var(--state-danger-bg); color: var(--sys-zinc-100); }
.m3-btn--tone-danger.m3-btn--outlined { color: var(--state-danger-fg); border-color: var(--state-danger-stroke); }
.m3-btn--tone-danger.m3-btn--text     { color: var(--state-danger-fg); }
.m3-btn--tone-danger.m3-btn--tonal    { background: var(--sys-red-500-12); color: var(--state-danger-fg); }

/* IconButton */
.m3-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ctl-h-default);
  height: var(--ctl-h-default);
  padding: 0;
  border: none;
  border-radius: var(--ctl-shape-pill);
  font-size: var(--icon-md);
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--dur-short-2) var(--ease-standard),
              color var(--dur-short-2) var(--ease-standard);
}
.m3-icon-btn:disabled          { opacity: 0.38; cursor: default; pointer-events: none; background: var(--card-fill-zinc); color: var(--text-muted); border: none; }
/* Variants — promoted rest (= old hover) per design spec; explicit
   `:hover` rules dropped.  `--standard` keeps a transparent rest — on
   hover the BG stays transparent (no container highlight) and only the
   GLYPH color lifts to the canonical bluepurple-300 hex (`#595fb4`),
   so the toolbar / map-control icon reads as a vibrant icon-only
   affordance rather than a lit-up tile.  The global :hover rule (which
   normally paints `--state-hover-bg` over every M3 control) excludes
   `--standard` via `:not(.m3-icon-btn--standard)` so the transparent
   bg sticks. */
.m3-icon-btn--standard         { background: transparent; color: var(--text); }
/* `--text-accent` gives WCAG-passing hover lift for the standard icon button. */
.m3-icon-btn--standard:hover   { background: transparent; color: var(--text-accent); }
.m3-icon-btn--filled           { background: var(--state-filled-bg); color: var(--sys-zinc-100); }
/* Tonal variant — -300 @ 15% bg + `--sys-bluepurple-300` glyph
   (bright -300 in dark / dim, palette-pivoted to deep -800 in light);
   mirrors `.m3-btn--tonal`. */
.m3-icon-btn--tonal            { background: var(--state-tonal-bg); color: var(--sys-zinc-100); }
/* Canonical M3 outlined — transparent fill, 2 px solid stroke + matching
   glyph, both anchored on the theme-stable bluepurple-400 hex.  Mirrors
   `.m3-btn--outlined` (stroke bumped from 1 px to 2 px per design spec
   so the outlined ring reads as a peer of the bluepurple-300 fill
   roster); bevel `::after` suppressed by the rounded-controls bevel
   block below. */
.m3-icon-btn--outlined         { background: var(--state-outlined-bg); color: var(--state-outlined-fg); border: 2px solid var(--state-outlined-stroke); }
/* Neutral variant — see .m3-btn--neutral above for the contextual
   fill rule.  Same token contract; foreground uses --ctl-neutral-fg
   (zinc-100 theme-stable bright glyph). */
.m3-icon-btn--neutral          { background: var(--ctl-neutral-bg-hover); color: var(--ctl-neutral-fg); }

/* ── Danger tone (icon button) ───────────────────────────────────────
   Mirrors `.m3-btn--tone-danger.*` above.  Standard variant keeps a
   transparent background and lifts only the GLYPH color to the red
   anchor (same chrome-quiet contract `--standard` carries for the
   accent tone).  Hover paints in `contracts.css`. */
.m3-icon-btn--tone-danger.m3-icon-btn--filled   { background: var(--state-danger-bg); color: var(--sys-zinc-100); }
.m3-icon-btn--tone-danger.m3-icon-btn--outlined { color: var(--state-danger-fg); border-color: var(--state-danger-stroke); }
.m3-icon-btn--tone-danger.m3-icon-btn--standard { background: transparent; color: var(--state-danger-fg); }
.m3-icon-btn--tone-danger.m3-icon-btn--tonal    { background: var(--sys-red-500-12); color: var(--state-danger-fg); }

/* FAB */
.m3-fab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  border: none;
  cursor: pointer;
  font-family: var(--font);
  background: var(--accent-subtle);
  /* `--text-accent` passes WCAG AA; `--accent-light` (bluepurple-400) failed on dark/dim. */
  color: var(--text-accent);
  box-shadow: var(--elevation-3);
  transition: box-shadow var(--dur-short-3) var(--ease-standard),
              background var(--dur-short-3) var(--ease-standard);
}
.m3-fab:hover {
  box-shadow: var(--elevation-4);
}
.m3-fab--default  { width: 56px; height: 56px; border-radius: var(--shape-lg); font-size: var(--icon-md); }
.m3-fab--sm       { width: 40px; height: 40px; border-radius: var(--shape-md); font-size: var(--icon-md); }
.m3-fab--lg       { width: 96px; height: 96px; border-radius: var(--shape-xl); font-size: 36px; }
.m3-fab--extended {
  height: 56px;
  border-radius: var(--shape-lg);
  padding-inline: var(--sp-4);
  font-size: var(--fs-label-lg);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label-lg);
}
.m3-fab__icon  { flex-shrink: 0; display: flex; align-items: center; }
.m3-fab__label { white-space: nowrap; }

/* SegmentedButton */
.m3-segmented {
  display: inline-flex;
  border-radius: var(--ctl-shape-rect);
  overflow: hidden;
}
.m3-segmented__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--ctl-h-default);
  padding-inline: var(--ctl-pad-inline);
  border: none;
  background: transparent;
  color: var(--text);
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  font-weight: var(--ctl-fw);
  letter-spacing: var(--ctl-ls);
  cursor: pointer;
  position: relative;
  transition: background var(--dur-short-2) var(--ease-standard),
              color var(--dur-short-2) var(--ease-standard);
}
.m3-segmented__btn + .m3-segmented__btn { border-left: 1px solid var(--border); }
.m3-segmented__btn:hover { background: var(--surface-hover); }
/* Selected segmented btn — same tonal recipe as `.m3-btn--tonal`:
   bluepurple-300 @ 15% bg + `--sys-bluepurple-300` glyph (bright -300
   in dark / dim, palette-pivoted to deep -800 in light) so the entire
   tonal family reads as one accent treatment. */
.m3-segmented__btn--selected       { background: var(--state-tonal-bg); color: var(--sys-zinc-100); }
.m3-segmented__btn--selected:hover  { background: var(--state-tonal-bg-hover); }

/* Compact variant — matches Select / Chip / TextField at 32 dp.  Use
   when the segmented button sits beside one of those compact controls
   so adjacent baselines align (e.g. the /node/* Packet Analyzer Rx /
   Tx / Total toggle next to its time-range Select). */
.m3-segmented--compact .m3-segmented__btn {
  height: var(--ctl-h-compact);
  padding-inline: var(--ctl-pad-inline-compact);
  font-size: var(--ctl-fs-compact);
  line-height: var(--lh-label-md);
  letter-spacing: var(--ls-label-md);
}

/* Badge — M3 notification overlay (dot/count anchored to an icon).
   Distinct from `.m3-tag` below — Badge is the absolute-positioned
   indicator that floats over an avatar / icon to signal unread state
   or count.  Reach for `.m3-tag` when you want an INLINE categorical /
   semantic pill (e.g. status chip, identifier tag, region badge). */
.m3-badge-anchor { position: relative; display: inline-flex; }
.m3-badge {
  position: absolute;
  z-index: 1;
  border-radius: var(--shape-full);
  font-size: var(--fs-label-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label-sm);
  line-height: 1;
}
.m3-badge--dot {
  width: 6px; height: 6px;
  top: 0; right: 0;
  background: var(--sys-red-500);
}
.m3-badge--label {
  min-width: 16px; height: 16px;
  top: -4px; right: -4px;
  padding-inline: var(--sp-1);
  display: flex; align-items: center; justify-content: center;
  background: var(--sys-red-500);
  color: var(--sys-white);
}

/* ════════════════════════════════════════════════════════════════════════════
   Tag — canonical INLINE categorical / semantic pill recipe.

   Distinct from `.m3-badge` (which is the M3 notification overlay) and
   from `.m3-chip` (which is an interactive 32-dp control with hover /
   select / delete affordances).  `.m3-tag` is a NON-interactive label
   pill — used for status indicators, identifier badges, type / region
   tags, alpha / beta callouts, and any other inline categorical label
   that doesn't carry the chip's interaction contract.

   The recipe consolidates the chrome shared by ~12 hand-rolled badge
   classes across the system (`.nav-pre-alpha-badge`, `.hero-badge`,
   `.tab-badge`, `.account-provider-badge`, `.net-rank-stack__count-
   badge`, generic `.pkt-badge` stops, admin's `.badge` / `.badge-*`,
   etc.).  Those hand-rolled classes are NOT migrated as part of this
   phase — they keep their per-page recipes and the consolidation is
   opt-in for future consumers.  `.m3-tag` is the canonical for any
   NEW inline categorical label.

   Modifier vocabulary (orthogonal):
     SIZE   : (default = md, 18 px / label-sm)  --sm (16 px / label-xs)
              --lg (22 px / label-md)
     SHAPE  : (default = rect, --shape-xs)      --pill (--shape-full)
              — rect is canonical for ALL inline data badges and
              matches the frontend's `.pkt-badge`, `.obs-status-badge`,
              `.nav-pre-alpha-badge`, `.net-type-badge` family.
              Pill is an explicit opt-in for hero / CTA pill callouts
              only (e.g., `.hero-badge` focus-ring chip).
     FAMILY : (default = sans)                   --mono (DM Mono @ 500)
     TONE   : (default = neutral)                --accent  --success
              --warning  --danger  --info
     BEHAVIOR: --uppercase  --with-dot (leading 6 px status dot)

   See AGENT.md "Inline Tag pill" for the canonical reach-for-this
   matrix and the domain-categorical badge migration roadmap.       */
.m3-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  height: 18px;
  padding-inline: var(--sp-2);
  /* Canonical badge rounding — anchors on `--shape-xs` (4 px), the
     value the frontend's hand-rolled badge family uses across
     `.pkt-badge`, `.obs-status-badge`, `.nav-pre-alpha-badge`,
     `.net-type-badge`, admin's `.badge`.  Reads as a crisp data
     token rather than a soft chip. */
  border-radius: var(--shape-xs);
  font-size: var(--fs-label-sm);
  line-height: 1;
  font-weight: var(--fw-medium);
  font-family: var(--font);
  letter-spacing: var(--ls-label-sm);
  white-space: nowrap;
  user-select: none;
  border: 1px solid transparent;
}
/* Size modifiers */
.m3-tag--sm { height: 16px; font-size: var(--fs-label-xs); letter-spacing: var(--ls-label-xs); padding-inline: var(--sp-1); }
.m3-tag--lg { height: 22px; font-size: var(--fs-label-md); letter-spacing: var(--ls-label-md); padding-inline: var(--sp-3); }
/* Shape modifier (default is rect / boxy; --pill opts into the soft
   chip CTA-pill variant used by hero focus-ring callouts only). */
.m3-tag--pill { border-radius: var(--shape-full); }
/* Family modifier — DM Mono caps at 500 (no synthesized bold). */
.m3-tag--mono { font-family: var(--mono); font-weight: var(--fw-medium); }
/* Behavior modifiers */
.m3-tag--uppercase { text-transform: uppercase; }
.m3-tag--with-dot::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--shape-full);
  background: currentColor;
  flex-shrink: 0;
}
/* Semantic tones — each paints bg + fg + (optional) stroke from the
   canonical Badge Color Map declared in `:root` (`--badge-{tone}-bg`
   / `--badge-{tone}-fg` / `--badge-{tone}-stroke`).  Re-tuning a tone
   across every consumer is a one-line change at the token map.  The
   tone tokens themselves resolve to wheel-family anchor stops per the
   design contract (see AGENT.md "Theme-agnostic anchor stops"); the
   light-mode palette pivot handles per-theme legibility automatically. */
.m3-tag--neutral {
  background: var(--badge-neutral-bg);
  color: var(--badge-neutral-fg);
  border-color: var(--badge-neutral-stroke);
}
.m3-tag--accent {
  background: var(--badge-accent-bg);
  color: var(--badge-accent-fg);
  border-color: var(--badge-accent-stroke);
}
.m3-tag--success {
  background: var(--badge-success-bg);
  color: var(--badge-success-fg);
}
.m3-tag--warning {
  background: var(--badge-warning-bg);
  color: var(--badge-warning-fg);
}
.m3-tag--danger {
  background: var(--badge-danger-bg);
  color: var(--badge-danger-fg);
}
.m3-tag--info {
  background: var(--badge-info-bg);
  color: var(--badge-info-fg);
}

/* ProgressIndicator */
.m3-progress { overflow: hidden; }
.m3-progress--linear {
  width: 100%; height: 4px;
  min-width: 0;
  border-radius: var(--shape-full);
  background: var(--surface);
}
.m3-progress__bar {
  height: 100%;
  border-radius: var(--shape-full);
  background: var(--accent);
  transition: width var(--dur-short-4) var(--ease-standard);
}
.m3-progress--linear.m3-progress--indeterminate .m3-progress__bar {
  width: 50%;
  animation: m3-progress-slide 2s var(--ease-standard) infinite;
}
@keyframes m3-progress-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(300%); }
}
.m3-progress--circular {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.m3-progress__track-circle { fill: none; stroke: var(--surface); stroke-width: 4; }
.m3-progress__circle       { fill: none; stroke: var(--accent); stroke-width: 4; stroke-linecap: round; }
.m3-progress--circular.m3-progress--indeterminate svg {
  animation: m3-circular-rotate 2s linear infinite;
}
.m3-progress--circular.m3-progress--indeterminate .m3-progress__circle {
  animation: m3-circular-dash 1.5s var(--ease-standard) infinite;
}
@keyframes m3-circular-rotate { 100% { transform: rotate(360deg); } }
@keyframes m3-circular-dash {
  0%   { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
  50%  { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
  100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
}

/* Indicator (status dot / line) */
.m3-ind {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  padding: var(--sp-1);
}
.m3-ind::after {
  content: '';
  display: block;
  background: var(--ind-color);
}
.m3-ind--dot::after              { width: 4px; height: 4px; border-radius: var(--shape-full); }
.m3-ind--line.m3-ind--horizontal::after { width: 10px; height: 2px; border-radius: var(--shape-full); }
.m3-ind--line.m3-ind--vertical::after   { width: 2px; height: 10px; border-radius: var(--shape-full); }
.m3-ind--micro.m3-ind--dot::after              { width: 2px; height: 2px; }
.m3-ind--micro.m3-ind--line.m3-ind--horizontal::after { width: 5px; height: 1px; }
.m3-ind--micro.m3-ind--line.m3-ind--vertical::after   { width: 1px; height: 5px; }
/* Continuous: smooth opacity pulse (connecting / activity) */
.m3-ind--pulse::after {
  animation: m3-ind-pulse 1.2s ease-in-out infinite;
}
/* Continuous: hard 1.5 s on / 1.5 s off (disconnected / error) */
.m3-ind--blink::after {
  animation: m3-ind-blink 3s step-start infinite;
}
/* One-shot: single intensity flash triggered imperatively */
.m3-ind--flash::after {
  animation: m3-ind-flash 0.2s ease-out;
}
@keyframes m3-ind-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}
@keyframes m3-ind-blink {
  0%  { opacity: 1; }
  50% { opacity: 0; }
}
@keyframes m3-ind-flash {
  0%   { opacity: 0.1; }
  100% { opacity: 1; }
}

/* Snackbar
   Motion-driven enter/exit — see `src/components/material/Snackbar.tsx`
   and `transitions.fade` in `src/lib/motion/config.ts`.  CSS carries
   layout + colour only; horizontal centering is handled via
   `style={{ x: "-50%" }}` on the motion.div (so Motion's `y` transform
   composes cleanly with the centering translate). */
.m3-snackbar {
  position: fixed;
  bottom: var(--sp-6);
  left: 50%;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 280px;
  max-width: 560px;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--shape-sm);
  background: var(--text);
  color: var(--bg);
  font-size: var(--fs-body-md);
  letter-spacing: var(--ls-body-md);
  z-index: var(--z-toast);
  pointer-events: auto;
}
.m3-snackbar--show { pointer-events: auto; }
.m3-snackbar__message { flex: 1; }
.m3-snackbar__action {
  flex-shrink: 0;
  padding: var(--sp-1) var(--sp-2);
  border: none;
  background: transparent;
  /* `--text-accent` passes WCAG AA; `--accent-light` failed at label-lg size. */
  color: var(--text-accent);
  font-size: var(--fs-label-lg);
  font-weight: var(--fw-medium);
  cursor: pointer;
  border-radius: var(--shape-sm);
}
.m3-snackbar__action:hover { background: var(--surface-tint-3); }
.m3-snackbar__close {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border: none;
  background: transparent;
  color: inherit;
  font-size: var(--icon-sm);
  cursor: pointer;
  border-radius: var(--shape-full);
  opacity: 0.7;
}
.m3-snackbar__close:hover { opacity: 1; background: var(--surface-tint-3); }


}

/* ── ui-kit.components: src/styles/components/modal.css ── */
@layer ui-kit.components {
/* ── Canonical modal/popup/sliding-shelf chrome (dialog, sheet, menu, nav-drawer) ─ */

/* ═══ Canonical modal / popup / sliding-shelf container chrome ════════════════════
   Translucent frosted-glass chrome for the floating-surface layer
   (dialogs, sheets, drawers, menus).  Applied via the chained
   selector below to every M3 modal / popover / shelf primitive:
     • `.m3-dialog`       — base modal dialog (login, confirm, snack-style)
     • `.m3-dialog--wide` — detail-style modal dialogs (PacketDetailModal)
     • `.m3-sheet`        — M3 bottom sheet (sliding shelf)
     • `.m3-menu`         — M3 popover / dropdown menu
     • `.m3-nav-drawer`   — hamburger menu side drawer (sliding shelf)
   New floating surfaces (popovers, command palettes, slide-overs)
   should adopt `.m3-modal-glass` directly (or be chained into this
   rule) so the entire modal layer reads as one canonical recipe.

   Recipe — translucent fill + backdrop blur + glass stroke + soft
   shadow.  This is the SAME recipe every live-map floating glass
   surface uses (`.map-pkt-ticker`, `.map-search-bar`,
   `.map-search-dropdown`, `.map-info-card`, `.map-ctrl-glass`,
   `.map-stats-glass`, `.nav--map`) and the LivePackets filter
   dropdowns (`.pkt-col-popover`); those app-class names chain into
   the same recipe via `frontend/src/styles/kit-aliases.css` so the
   whole floating-surface family reads as ONE frosted voice.

   Per-theme overrides below mirror the live-map family:
     • Dark:  lifts `border-color` to zinc-700 — one step brighter than
              the zinc-900 fill so the modal edge reads cleanly.
     • Dim:   same zinc-700 border; the warm-dark fill reads the same
              perceptual story.
     • Light: `--outline-glass` (zinc-400) sits clearly darker than the
              solid `--bg-raised` fill; no override needed.  `backdrop-
              filter` is also dropped (solid fill makes the blur pass
              redundant and it frees GPU budget).

   IMPORTANT: this treatment is for transient floating surfaces ONLY.
   Parent containers (cards, panels, page sections) reach for the
   `.m3-card` family directly — see AGENT.md "Card system" for the
   parent-container vocabulary. */
.m3-modal-glass,
.m3-dialog,
.m3-dialog--wide,
.m3-sheet,
.m3-menu,
.m3-nav-drawer {
  background: var(--surface-glass);
  backdrop-filter: var(--blur-md);
  border: 1px solid var(--outline-glass);
  box-shadow: var(--elevation-2);
}

/* Dark theme — modal border lifted to zinc-700 so it reads as a
   distinct edge above the zinc-900 75%-opacity fill.  The shared
   `--outline-glass` token (zinc-800) is intentionally one step too
   dark for this purpose; the modal layer needs the extra contrast. */
:root[data-theme="dark"] :is(
  .m3-modal-glass,
  .m3-dialog,
  .m3-dialog--wide,
  .m3-sheet,
  .m3-menu,
  .m3-nav-drawer
),
:root:not([data-theme]) :is(
  .m3-modal-glass,
  .m3-dialog,
  .m3-dialog--wide,
  .m3-sheet,
  .m3-menu,
  .m3-nav-drawer
) {
  border-color: var(--sys-zinc-700);
}

/* Dim theme — restore border at zinc-700; the warm-dark #1C1C1B fill
   reads the same story as dark and needs the same lifted edge.
   Previous rule dropped this to transparent; that was correct when
   the goal was invisible chrome, but with an intentional modal border
   zinc-700 sits clearly above the surface without feeling harsh. */
:root[data-theme="dim"] :is(
  .m3-modal-glass,
  .m3-dialog,
  .m3-dialog--wide,
  .m3-sheet,
  .m3-menu,
  .m3-nav-drawer
) {
  border-color: var(--sys-zinc-700);
}

/* Light theme — `--surface-glass` resolves to solid `--bg-raised`,
   so the backdrop blur pass is redundant.  Dropping it frees the
   compositing pass + eliminates the subtle tone shift backdrop blur
   introduces under an opaque fill. */
:root[data-theme="light"] :is(
  .m3-modal-glass,
  .m3-dialog,
  .m3-dialog--wide,
  .m3-sheet,
  .m3-menu,
  .m3-nav-drawer
) {
  backdrop-filter: none;
}

/* Dialog
   Motion-driven enter/exit — see `src/components/material/Dialog.tsx`
   and the canonical `transitions.modal` / `transitions.backdrop` /
   `variants.scrim` / `variants.dialog` exports in
   `src/lib/motion/config.ts`.  The CSS rules below carry layout +
   `pointer-events` only; opacity / transform are driven by Motion.

   DOM structure (3 layers):
     .m3-dialog-overlay   — fixed scrim, Motion-animates opacity
     .m3-dialog-anim      — animation wrapper, Motion-animates
                           scale + opacity (centred by the scrim's
                           flex layout)
     .m3-dialog           — static glass surface (`.m3-modal-glass`
                           chain paints translucent fill + backdrop
                           blur + stroke + shadow). */
/* Scrim carries BOTH the dim wash (`--overlay`) and a viewport-wide
   `backdrop-filter` so the page beneath reads as frosted + dimmed.
   The `.m3-dialog` glass on top adds its own translucent fill on top
   of the already-blurred scrim; the slight blur compounding is
   intentional — the area directly behind the dialog reads as a
   deeper frost than the area visible around it through the scrim
   alone, mirroring the perceptual hierarchy of a real frosted pane
   over a softly-frosted background. */
.m3-dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--overlay);
  backdrop-filter: var(--blur-sm);
  pointer-events: none;
}
.m3-dialog-overlay--open { pointer-events: auto; }
/* Animation wrapper — sized to fit the inner static dialog, scaled +
   faded by Motion.  No glass treatment lives here — the inner
   `.m3-dialog` carries the canonical `.m3-modal-glass` chrome. */
.m3-dialog-anim {
  display: flex;
  flex-direction: column;
  max-width: 100%;
  max-height: 100%;
}
/* Base dialog — layout-only here.  Chrome (translucent glass fill +
   backdrop blur + 1px stroke + elevation-2 shadow) is painted by the
   chained `.m3-modal-glass` rule above. */
.m3-dialog {
  min-width: 280px;
  max-width: 560px;
  max-height: 80vh;
  border-radius: var(--shape-xl);
  padding: var(--sp-6);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
/* Wide variant — detail dialogs (PacketDetailModal) that need more
   horizontal real estate than the M3 default 560px.  Both width AND
   height are locked per breakpoint so the dialog stays the same size
   as the user navigates between packets with different content shapes
   (single-column collapse, varying Identifier-tab row counts, etc.).
   The inner `.m3-dialog__content` flexes to fill the remaining
   vertical space and scrolls when content exceeds the locked height.

  Chrome (translucent glass fill + backdrop blur + stroke + shadow)
   is painted by the chained `.m3-modal-glass` rule above; this rule
   carries layout only. */
.m3-dialog--wide {
  width: min(960px, 90vw);
  max-width: min(960px, 90vw);
  height: min(720px, 88vh);
  max-height: min(720px, 88vh);
}
.m3-dialog--wide .m3-dialog__content {
  flex: 1;
  min-height: 0;
}
@media (max-width: 839px) {
  .m3-dialog--wide {
    width: 92vw;
    max-width: 92vw;
    height: 88vh;
    max-height: 88vh;
  }
}
@media (max-width: 599px) {
  /* Compact tier: the wide dialog reshapes into an M3 bottom sheet —
     pinned to the bottom edge, full-bleed, slides up on open, with a
     decorative drag-handle indicator across the top.  Mirrors the
     live-map info card pattern (`.map-info-card` @ ≤ 599px) so users
     get the same touch-shelf affordance for any large detail dialog. */
  .m3-dialog--wide {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 100%;
    height: auto;
    min-height: 60dvh;
    max-height: 92dvh;
    margin: 0;
    border-radius: var(--shape-xl) var(--shape-xl) 0 0;
    padding-top: var(--sp-3);
    padding-bottom: calc(var(--sp-6) + env(safe-area-inset-bottom, 0px));
    box-shadow: var(--elevation-3);
    /* Slide-up on Compact is now handled by Motion (Dialog overrides
       its dialog variant via inline style at this breakpoint).  The
       CSS keeps layout only; Motion owns the transform. */
  }
  /* Drag-handle indicator — decorative bar at the top of the sheet,
     identical to .map-info-card__handle / .m3-sheet__handle.  Rendered
     as a ::before so we don't have to thread a slot through the Dialog
     component for a purely visual cue. */
  .m3-dialog--wide::before {
    content: '';
    align-self: center;
    width: 32px;
    height: 4px;
    border-radius: var(--shape-full);
    background: var(--text-muted);
    opacity: 0.4;
    flex-shrink: 0;
  }
}
.m3-dialog__title {
  font-size: var(--fs-headline-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-headline-sm);
}
.m3-dialog__content {
  font-size: var(--fs-body-md);
  line-height: var(--lh-body-md);
  letter-spacing: var(--ls-body-md);
  color: var(--text-muted);
  overflow-y: auto;
}
.m3-dialog__actions { display: flex; justify-content: flex-end; gap: var(--sp-2); }

/* BottomSheet
   Motion-driven enter/exit — see `src/components/material/BottomSheet.tsx`
   and `transitions.shelfEnter` / `transitions.shelfExit` /
   `variants.sheetUp` in `src/lib/motion/config.ts`.  The CSS rules
   below carry layout + `pointer-events` only; transform / opacity
   live in Motion.

   DOM structure (3 layers):
     .m3-sheet-overlay  — fixed scrim, Motion-animates opacity
     .m3-sheet-anim     — bottom-anchored animation wrapper,
                          Motion-animates translateY (slide up)
     .m3-sheet          — static glass surface inside the wrapper */
/* Scrim carries the dim wash + a viewport-wide `backdrop-filter` so
   the page beneath reads as frosted + dimmed (see `.m3-dialog-overlay`
   for the full rationale). */
.m3-sheet-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--overlay);
  backdrop-filter: var(--blur-sm);
  pointer-events: none;
}
.m3-sheet-overlay--open { pointer-events: auto; }
/* Animation wrapper — bottom-anchored full-width box that Motion
   slides up via `translateY(100%) → 0`.  No glass treatment here;
   the inner `.m3-sheet` carries the canonical `.m3-modal-glass`
   chrome. */
.m3-sheet-anim {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: calc(var(--z-modal) + 1);
  max-height: 90vh;
  display: flex;
  flex-direction: column;
}
/* Bottom sheet — layout-only here.  Chrome (translucent glass fill +
   backdrop blur + 1px stroke + elevation-2 shadow) is painted by the
   chained `.m3-modal-glass` rule above. */
.m3-sheet {
  max-height: 90vh;
  border-radius: var(--shape-xl) var(--shape-xl) 0 0;
  display: flex;
  flex-direction: column;
}
.m3-sheet__handle {
  display: flex;
  justify-content: center;
  padding: var(--sp-3) 0 var(--sp-2);
}
.m3-sheet__handle::after {
  content: '';
  width: 32px; height: 4px;
  border-radius: var(--shape-full);
  background: var(--text-muted);
  opacity: 0.4;
}
.m3-sheet__content {
  padding: 0 var(--sp-6) var(--sp-6);
  overflow-y: auto;
  flex: 1;
}


}

/* ── ui-kit.components: src/styles/components/card.css ── */
@layer ui-kit.components {
/* ── Unified card system: .m3-card + chrome modifiers + clickable + elevated ─ */

/* ═══ Unified Card System (Liquid-Glass Chrome) ═══════════════════════════════════
   ONE governing class (`.m3-card`) carries the canonical "liquid-glass"
   treatment by default — a single soft inner dome-glow + a hairline
   gradient bevel rim, painted onto a fill that matches the page `--bg`
   so the card reads as a pane of glass laid flat on the surface rather
   than a tonally-lifted panel.  Adapted 1:1 from the Figma "glass
   example" swatch (Untitled, node 1891:36).  Three orthogonal modifier
   groups compose on top:

     1. CHROME  (mutually exclusive, default = raised):
          • (no modifier)         — canonical raised (the system standard)
          • .m3-card--flat-inset  — inverted (sunken) gradient outline
          • .m3-card--flat        — plain fill, no outline, no shadows

     2. FILL    (composable; only meaningful with `--flat`):
          • .m3-card--fill-bluepurple-tonal  — `--glow` (bluepurple-500 @ 15%)

     3. CONTEXT (automatic via descendant selector): a `.m3-card`
        nested inside another `.m3-card` auto-steps to flat-raised so
        nested panels read as a subdued continuation of the parent.
        Override by adding an explicit chrome modifier to the child.

   FILL — `--container-bg` paints `--bg` (NOT `--surface`) on the
   padding-box, so a card is the same colour as the page it sits on.
   It is separated from the page purely by the bevel rim + dome-glow,
   the glass look; there is no tonal step between card and background.

   RIM — the second `--container-bg` layer paints a hairline gradient
   bevel on the border-box (tokenized as `--container-bevel` so the
   resting recipe and both hover states share one source of truth): a
   `to bottom right` sweep, stop position @ opacity —
   `--container-bevel-bright` (0% @ 80%) →
   `--container-bevel-mid` (32% @ 33%) →
   `--container-bevel-mid` (82% @ 66%) →
   `--container-bevel-bright` (100% @ 50%),
   i.e. a bright top-left corner, a body that fades toward translucent
   mid-grey, and a faded bright return at the bottom-right — a single
   directional rim light whose corners read strongest.  The `to bottom right` keyword (NOT a fixed angle) pins the
   0%/100% stops to the actual TL/BR corners at ANY aspect ratio, so a
   long rank-row rim stays corner-anchored instead of stretching the
   bright into a wedge at one corner — matching how the Figma gradient
   re-anchors on resize.  Bright/mid default to `#ABABAB` / `#454545`
   (dim + dark) and invert to zinc-50 / zinc-500 in light.  The rim
   WIDTH is the `--container-stroke` hairline token (0.66px) — shared by
   `--container-border` (cards) and the control bevel `::after` rings
   (contracts.css).  NOTE: the control rings are `padding`-based and
   honour the fractional value exactly (0.66px = 1.32 device-px on retina,
   which smooths the rounded-corner aliasing a pure 1-device-px hairline
   shows).  Card rims are a real CSS `border`, and browsers ROUND
   border-width to whole device pixels — so on a 2x display the card
   border still lands on 0.5px (1 device-px) until the token reaches
   1px (2 device-px).  Cards therefore step 0.5px<->1px only; the
   fractional smoothing applies to the control rings.

   Shadow anatomy (`--shadow-container`, three layers):
     Layer 1 — INNER DOME (`--shadow-elev-inner-glow`): the Figma inner
               shadow — `inset 7px 14px 64.1px -46px` white.  A soft,
               top-biased glow washing the upper interior and fading to
               nothing at the bottom, giving the pane a gently convex
               top-lit read.  White @ 18% in dim + dark; near-zero in
               light (a white glow is invisible on a light surface,
               which is correct — light cards are carried by the bevel).
     Layer 2 — AMBIENT OCCLUSION (`--shadow-elev-occlusion`): soft outer
               shadow directly below.  The panel floats ~1 px off the
               chassis; a tight dark halo hugs the bottom perimeter.
     Layer 3 — DEPTH DROP (`--shadow-elev-depth`): broader, softer outer
               shadow.  Low alpha — reads as atmosphere, not a hard edge.

   Retired in the `glass` pass: the razor-thin TOP-CATCH + BOTTOM-REFLECT
   hairlines and the INNER-SHADE dark wash.  The top-catch/bottom-reflect
   were an older way of faking a rim light before the gradient bevel
   existed; stacked against the bevel they read as a doubled 2px outline.
   The single gradient rim now owns the edge lighting.

   Per-theme tuning happens via the `--shadow-elev-*` slots (see
   `tokens/semantic.tokens.json` + per-theme overrides in
   `tokens/themes/*.tokens.json`).  Dark surfaces swallow light →
   heavier outer shadows.  Light surfaces reflect → softer shadows.

   Radius is pinned to `--shape-lg` so every card and every legacy
   alias (`.pkt-card`, `.feature-card`, `.connect-card`,
   `.connect-section`, `.edge-shelf-disclosure`, `.outpost-obs-card`,
   `.outpost-section-card`, `.about-feature`, `.account-card`,
   `.nd-status-card`, `.net-rank-row`) shares the same corner.  See
   AGENT.md "Card system" row for the full per-legacy-class mapping. */
/* Padded modifier — paints the canonical inner padding on the card body.
   The `.m3-card` recipe deliberately leaves padding to the consumer so
   cards with their own internal chrome (ChartCard, ObserverCard,
   MQTT-events flat-inset wrapper) can manage their own rhythm.  Use
   `padded` when the card wraps bare content (a table, a stat block, a
   section body) that needs the standard inset against the outline ring.
   Replaces the previous inline `style={{ padding: "var(--card-pad)" }}`
   chain consumers hand-rolled for that case. */
.m3-card--padded {
  padding: var(--card-pad);
}

/* Canonical chrome painter — targets `.m3-card` only.  Legacy app-class
   aliases that historically composed this chrome (`.feature-card`,
   `.pkt-card`, `.edge-shelf-disclosure`, `.connect-card`,
   `.connect-section`, `.net-rank-row`, `.nd-status-card`,
   `.outpost-stat-card`, etc.) opt in via the app-side roster sheet
   `frontend/src/styles/kit-aliases.css` (Phase 5f canonical-roster
   restructure).  The kit no longer enumerates app aliases. */
.m3-card {
  border-radius: var(--shape-lg);
  overflow: hidden;
  background: var(--container-bg);
  border: var(--container-border);
  box-shadow: var(--shadow-container);
  transition: box-shadow var(--dur-short-3) var(--ease-standard),
              background var(--dur-short-3) var(--ease-standard);
}

/* ── Chrome modifiers ─────────────────────────────────────────────
   Each modifier strips a layer from the default raised treatment.
   The fill defaults to `--surface` once the gradient outline is
   dropped (via `--flat`) so the card still reads as a parent
   container, just without the lift cues. */
.m3-card--flat-inset {
  /* Swap to the inverted gradient — darker stroke on top, brighter
     on bottom — paired with `--surface-sunken` so the card reads as
     recessed below the page bg.  Mirrors the `.connect-code pre`
     treatment for code blocks and inset detail strips. */
  background: var(--container-bg-sunken);
  box-shadow: none;
}
.m3-card--flat {
  /* Plain fill, no chrome.  Default fill is `--surface` (the
     canonical parent-container neutral); compose with a `--fill-*`
     modifier to override with brand or tonal fills. */
  background: var(--card-fill-zinc);
  border: 1px solid transparent;
  box-shadow: none;
}

/* ── Fill modifier ────────────────────────────────────────────────────────
   Compose with `--flat` to swap the neutral surface fill for the
   accent-tinted tonal fill (wheel BLUE-PURPLE sector @ 15%). */
.m3-card--fill-bluepurple-tonal  { background: var(--card-fill-bluepurple-tonal); }

/* ── Context-aware nest step-down ──────────────────────────────────────
   When a `.m3-card` nests inside another `.m3-card`, the child
   automatically drops its shadow stack so the inner panel reads as
   a subdued continuation of the parent rather than a competing
   raised surface.  Authors can opt back into the full raised
   treatment on a nested child by adding any explicit chrome
   modifier; the `:not([class*="m3-card--"])` guard means an explicit
   `--flat-inset` / `--flat` / `--flat-raised` modifier on the child
   still wins on specificity.  This is the "context awareness"
   half of the parent/child system; the manual switch is the
   chrome modifier vocabulary above. */
.m3-card .m3-card:not([class*="m3-card--"]) {
  box-shadow: none;
}

/* ── Clickable + Elevated ────────────────────────────────────────────
   `--clickable` is a behavior modifier (cursor + hover wash).
   `--elevated` is the menu/drawer elevation recipe — it sits
   OUTSIDE the canonical card chrome family and is used by M3 menus,
   the bottom-sheet pill, and the map control glass cards.  Reach for
   `--elevated` when you need surface-1 elevation with the drop
   shadow only (no gradient bevel); reach for the unmodified
   `.m3-card` when you want a canonical parent container. */
/* ── Clickable: Inward Brighten + Gentle Lift ───────────────────────
   Hover recipe for `.m3-card--clickable`.  All cues focus INWARD so
   the card itself reads as luminous instead of being framed by a
   darker drop shadow / heavy ring (which would relatively darken the
   card by contrast).  Four cohesive cues:

     1. LIFT          — `translateY(-2 px)` (subtle Material 3 step).
     2. SURFACE WASH  — `--surface-tint-2` (theme-aware white-7 in dark /
                        dim, black-6 in light) composes ON TOP of the
                        canonical `--container-bg` so the surface
                        gently brightens — the SAME three-layer trick
                        the existing `.card--hover-wash` uses, just
                        one tint step warmer for clickable.
     3. BEVEL-LIFT    — the top-edge highlight stop steps 0.15 → 0.25
                        so the canonical gradient bevel reads as more
                        catchlit.
     4. WARM HALO     — a soft `var(--sys-bluepurple-300)` bloom with
                        wide negative spread, NOT a dark drop shadow.
                        Reads as warm light coming OFF the card, not
                        as a dark frame surrounding it.

   Augmenting the base `.m3-card` transition with a `transform` channel
   on `.m3-card--clickable` lets the lift animate smoothly without
   forcing every non-clickable card to ship a transform transition.
   All channels land at `var(--dur-short-3)` / emphasized-decel for a
   cohesive single-beat hover. */
.m3-card--clickable {
  cursor: pointer;
  transition: box-shadow var(--dur-short-3) var(--ease-emphasized-decel),
              transform  var(--dur-short-3) var(--ease-emphasized-decel),
              background var(--dur-short-3) var(--ease-emphasized-decel);
}
/* stylelint-disable declaration-property-value-disallowed-list -- canonical brighten recipe: opaque brighter zinc step + warm halo + accent ring; could be tokenized as --container-bg-hover + --shadow-container-hover in a follow-up */
.m3-card--clickable:hover {
  transform: translateY(-2px);
  /* OPAQUE brighter fill via `--bg-raised` (zinc.900 in dark, #30302F in
     dim, zinc.100 in light) — one zinc step brighter than the resting
     `--bg` fill in every theme.  An opaque fill is unambiguous in a way
     a semi-transparent overlay isn't: the surface visibly steps up
     regardless of how the overlay composites against the underlying
     theme bg.  The glass rim is kept identical to rest via
     `--container-bevel` so hovering brightens the FILL without the rim
     flipping to a different bevel treatment. */
  background:
    linear-gradient(var(--bg-raised), var(--bg-raised)) padding-box,
    var(--container-bevel);
  /* Inner-tier accent ring + canonical container shadow + warm halo.
     The accent ring is now painted via `inset box-shadow` (Inner tier
     per Foundations / Keylines & Strokes) so it stacks ADJACENT to the
     host's `--container-border` (Mid tier) instead of overwriting the
     same pixel layer.  Result: hover preview shows BOTH the canonical
     chrome border AND the accent state ring, layered cleanly. */
  box-shadow:
    var(--shadow-container),
    0 0 32px -10px var(--hover-halo),
    inset 0 0 0 var(--stroke-thick) var(--accent);
}
/* stylelint-enable declaration-property-value-disallowed-list */
.m3-card--elevated {
  /* Canonical surface fill + gradient bevel outline from the default
     raised recipe, but with the shadow stack stripped.  The gradient
     outline provides dimensionality (the -1deg black-to-white bevel
     reads as a subtle lit edge) WITHOUT the drop-shadow or inset
     highlight/shadow cues that the full raised treatment adds.  Use
     this when a card surface should feel grounded on the page rather
     than floating above it — the outline separates the surface from
     the bg, but the lack of shadow keeps the card visually flush.

     Inherits `background: var(--container-bg)` +
     `border: var(--container-border)` from the base `.m3-card` rule;
     only the shadow stack is overridden here. */
  box-shadow: none;
}


/* ── Selected + tone state rings (Inner tier) ──────────────────
   Orthogonal modifier ring painted via `inset box-shadow` so it sits
   on the Inner-tier layer (inside the host's border-box) per the
   Foundations / Keylines & Strokes taxonomy.  This stacks the state
   ring adjacent to the host's `--container-border` chrome (Mid tier)
   instead of overwriting the same pixel layer — hover / active /
   danger now read as ADDED on top of the canonical chrome rather than
   replacing its colour.

   Selected paints `var(--accent)` (UI-state ring); `tone="danger"`
   paints `var(--state-danger-stroke)` (data-state ring).  Source order
   places the danger rule AFTER selected so a card with BOTH classes
   resolves to the danger ring — data state outranks UI state.

   The selected / danger box-shadow has to RE-DECLARE the canonical
   `var(--shadow-container)` because CSS box-shadow is a single
   property that replaces wholesale (no comma-merge across rules).
   Keeping the original drop-shadow + adding the inset state ring
   preserves elevation while adding the state ring. */
.m3-card--selected {
  box-shadow:
    var(--shadow-container),
    inset 0 0 0 var(--stroke-thick) var(--accent);
}
.m3-card--tone-danger {
  box-shadow:
    var(--shadow-container),
    inset 0 0 0 var(--stroke-thick) var(--state-danger-stroke);
}

/* Phase 5f: `.map-ctrl-glass` + `.map-stats-glass` map-layer chrome
   moved to `frontend/src/styles/integrations/maplibre.css` (app-side).
   Kit no longer enumerates `.map-*` aliases. */

/* ── Live-spill wrapper + duplicate ─────────────────────────────
   When `<Card liveSpill={true}>` is rendered the React component wraps
   the canonical `.m3-card` in a `.m3-card-frame` that also holds an
   `aria-hidden` + `inert` duplicate of children as a SIBLING sitting
   behind the card.  The duplicate is the actual JSX children rendered
   a second time — not a simulated approximation — so the spill is a
   live mirror that updates as the card's content changes.

   GEOMETRY.  The frame is `position: relative` and DOES NOT clip
   overflow.  The card itself keeps its `overflow: hidden` (needed for
   its own bg-image stack to clip to rounded corners).  The spill is
   absolutely positioned at `inset: 0` of the frame (= same bounds as
   the card) with `filter: blur(24px)` smearing the duplicated content
   into a soft echo.  The blur radius pushes the visible halo PAST the
   card's edges; since the frame doesn't clip, the halo bleeds outside
   the card silhouette as the visible spill effect.

   Y-BIAS.  A `transform: translateY(36px)` shifts the entire spill
   DOWN by 36 px relative to the card.  The intent: the top edge of
   the card's bright top-catchlight, blurred at 24 px, was producing a
   fuzzy outer glow ABOVE the card silhouette that read as
   problematic.  Pushing the spill down moves that bright top-edge
   halo BELOW the card's own top, so the visible bleed becomes a
   bottom-biased ground-glow rather than an all-sides halo.

   The card paints on top of the spill (later in source order, no
   z-index needed) so the foreground content stays sharp while only
   the bleed-past-edges portion of the blurred duplicate is visible. */
.m3-card-frame {
  position: relative;
}
.m3-card__live-spill {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  /* Opacity trimmed 0.50 → 0.45 (−10 % relative) to soften the halo
     after the Y-bias landed the bright top-catchlight smear closer to
     the foreground content below the card. */
  opacity: 0.45;
  filter: blur(12px);
  /* Bias spill downward so the bright top-catchlight halo bleeds
     below the card rather than above it. */
  transform: translateY(18px);
}
.m3-card__live-spill,
.m3-card__live-spill * {
  pointer-events: none !important;
}

/* Dim theme drops the spill opacity another −10 % (relative) from the
   0.45 default → 0.405.  Dim's #262525 body has less luminance
   headroom than dark's near-black surface, so the blurred bright
   top-catchlight smear in the spill reads more prominently here.
   Trimming the alpha brings dim back into perceptual parity with the
   dark default's quieter halo. */
[data-theme="dim"] .m3-card__live-spill {
  opacity: 0.405;
}

/* Light theme drops the live-spill glow entirely.  The blurred
   ground-glow echo reads as a smudgy halo rather than a glow against
   a light surface — the effect depends on bleeding a BRIGHT blurred
   highlight into a darker page bg, which light theme's paper-toned
   bg doesn't provide.  Dark/dim keep their existing treatment
   unchanged; this only mutes the duplicate frame, not the card
   itself. */
[data-theme="light"] .m3-card__live-spill {
  display: none;
}

/* ── Live-spill bleed-allowance on host cards ──────────────
   When a `.m3-card` HOST contains an inner `.m3-card-frame` (i.e. a
   nested liveSpill-emitting card inside this card's body), allow the
   inner spill's blur halo to bleed past this host's `overflow: hidden`
   border-box by up to 80 px.  Without this, the dashboard-hero use
   case (`.net-today-hero` containing the bento grid of liveSpill
   tiles) sharply CLIPS the inner tiles' lateral + downward spill at
   the host card's edges — the bento tiles sit only ~16-20 px inside
   the host's padding, and the spill's 24 px blur + 36 px translateY
   pushes past that gap by tens of pixels.  `overflow-clip-margin`
   extends the clip region past the host's border-box so the spill
   tapers naturally beyond the host silhouette instead of cutting at
   a hard edge. */
.m3-card:has(.m3-card-frame) {
  overflow-clip-margin: 80px;
}


}

/* ── ui-kit.components: src/styles/components/stat-tile.css ── */
@layer ui-kit.components {
/* ── StatTile ────────────────────────────────────────────────────────
   Canonical chrome for the StatTile primitive
   (`packages/ui-kit/src/react/components/StatTile.tsx`).  Replaces the
   hand-rolled `.feature-card .nd-stat-card` recipe in
   `frontend/src/styles/pages/node-detail.css`.

   Two density tiers (`compact` default, `large` for hero stats).  Both
   share the column-stack body (label → value → delta) with optional
   icon and sparkline slots layered into the layout. */

.stat-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  text-align: center;
  position: relative;
  /* `min-width: 0` cascades down to the body + value spans so the
     flex layout shrinks the tile to its parent's track width (typical
     case: equal-width `<Pane>` panes inside a `<PaneRow>`).  Without
     this, a wide value glyph (`-76 dBm`, `87 / 88`, `active-active`)
     would lock the tile to its intrinsic content width and let it
     overflow the parent's `flex: 1` track, breaking the row's even
     rhythm.  See Storybook `Foundations/Layout § PaneRow stat tiles`. */
  min-width: 0;
  /* Default chrome inherits .feature-card / .m3-card from cascade when
     the consumer composes one of those onto the tile; the tile itself
     paints no outer chrome so it can sit inside any container. */
}

.stat-tile--compact {
  padding: var(--sp-4) var(--sp-3);
}

.stat-tile--large {
  padding: var(--card-pad);
}

.stat-tile__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* --text-accent = bluepurple-200 (dark/dim) / bluepurple-600 (light) —
     passes WCAG AA; --accent-light (bluepurple-400) failed at 2.55:1 on dim. */
  color: var(--text-accent);
  margin-bottom: var(--sp-1);
}

.stat-tile__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  /* Body MUST shrink with the outer flex track so the value glyph
     below can read `max-width: 100%` correctly.  Otherwise the body
     hugs its intrinsic content width and the value's overflow / nowrap
     contract never kicks in. */
  min-width: 0;
  max-width: 100%;
}

.stat-tile__label {
  font-size: var(--fs-label-md);
  line-height: var(--lh-label-md);
  /* Canonical eyebrow anchor \u2014 see `theme-extras.css \u00a7 Eyebrow
     color anchor`.  Aligns with `PartitionHeading` +
     `.metric-meter__label` so every eyebrow surface in the kit
     reads at the same theme-aware contrast rung. */
  color: var(--text-eyebrow);
  /* One rung lighter than the prior inherited body weight.  Matches
     the rest of the eyebrow family so a stat-tile label, partition
     heading, and metric-meter label all paint at the same weight. */
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
  /* Labels stay on one line so the tile body keeps a consistent
     two-row footprint (label + value) across every consumer.  When
     a long label ("Used by Observers") genuinely doesn't fit, the
     overflow clips with an ellipsis instead of wrapping. */
  white-space: nowrap;
  overflow: clip;
  text-overflow: ellipsis;
  max-width: 100%;
}

.stat-tile--large .stat-tile__label {
  font-size: var(--fs-body-md);
  line-height: var(--lh-body-md);
  letter-spacing: var(--ls-body-md);
  text-transform: none;
}

.stat-tile__value {
  font-size: var(--fs-headline-md);
  line-height: var(--lh-headline-md);
  letter-spacing: var(--ls-headline-md);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  /* Values are quantitative — they must stay on one line so multi-token
     readouts (`5.4 dB`, `-76 dBm`, `87 / 88`, `Active-Active`) don't
     wrap at intra-value spaces / hyphens and double the tile's height
     inside an equal-width `<PaneRow>` track.  `word-break: keep-all`
     keeps hyphenated single-words intact; `max-width: 100%` paired
     with `overflow: clip` + `text-overflow: ellipsis` gives a graceful
     truncation contract when a tile genuinely can't fit the value
     (instead of overflowing the tile's track and pushing siblings). */
  white-space: nowrap;
  word-break: keep-all;
  overflow-wrap: normal;
  overflow: clip;
  text-overflow: ellipsis;
  max-width: 100%;
}

.stat-tile--large .stat-tile__value {
  font-size: var(--fs-headline-lg, var(--fs-headline-md));
  line-height: var(--lh-headline-lg, var(--lh-headline-md));
}

.stat-tile__delta {
  font-size: var(--fs-body-xs);
  line-height: var(--lh-body-xs);
  letter-spacing: var(--ls-body-xs);
  font-variant-numeric: tabular-nums;
}

.stat-tile__badge {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
}

.stat-tile__sparkline {
  width: 100%;
  margin-top: var(--sp-2);
}

/* ── Hero variant ──
   Value-first marquee layout for stat tiles where the number IS the
   headline (e.g. Outpost fleet counters).  The body re-orders its
   children to value → label → subLabel.  Typography scales bump up
   to give the headline number visual weight.  Anchor accent-light is
   reused for the subLabel period text ("Last 24h", "all time", "now"),
   matching the existing Outpost `outpost-stat-period` rhythm. */
.stat-tile--hero .stat-tile__value {
  font-size: var(--fs-headline-lg, var(--fs-headline-md));
  line-height: var(--lh-headline-lg, var(--lh-headline-md));
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.stat-tile--hero .stat-tile__label {
  font-size: var(--fs-label-md);
  line-height: var(--lh-label-md);
  /* Hero variant of the stat-tile label stays on the canonical
     eyebrow anchor.  See `.stat-tile__label` above for the
     `--text-eyebrow` + `--fw-medium` retune rationale. */
  color: var(--text-eyebrow);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
}

.stat-tile__sub-label {
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  /* Use --text-accent for WCAG AA compliance at small label size; `--accent-light`
     (bluepurple-400 dark/dim) fails AA on dark/dim surfaces at this scale. */
  color: var(--text-accent);
  font-variant-numeric: tabular-nums;
  margin-top: var(--sp-0_5);
}

/* ── valueTone modifiers ────────────────────────────────────
   Point-in-time threshold tone on the value glyph.  Same
   `--data-sign-{up,down,neutral}` token chain `stat-tile__delta`
   reads, so a tile that paints both value + delta in the same tone
   reads as one unified signal.  Per `Patterns/Identity vs State
   Badging`: state stays on the value's COLOR slot, never inside the
   value text.

   Selector chain `.stat-tile .stat-tile__value--{tone}` is (0,2,0)
   to tie `.stat-tile--hero .stat-tile__value` (also 0,2,0) above.
   Source order resolves the tie in favor of these rules, so the
   valueTone color paints regardless of whether the tile is on the
   hero or default variant. */
.stat-tile .stat-tile__value--up {
  color: var(--data-sign-up, var(--sys-green-500));
}
.stat-tile .stat-tile__value--down {
  color: var(--data-sign-down, var(--sys-red-500));
}
.stat-tile .stat-tile__value--neutral {
  color: var(--data-sign-neutral, var(--text-muted));
}

/* ── severity stripe ────────────────────────────────────────
   Left accent rail giving a tile KPI-card semantics: a grid of tiles
   reads its alarm state at a glance without parsing numbers.  Painted
   as a `::before` (root is `position: relative`, line 17) so it never
   perturbs the flex box or the density padding.  `warn` is the ONE
   tone without a `--data-sign-*` alias, so it reaches the canonical
   orangeyellow ramp directly (matching the `<Tag tone="warning">`
   recipe). */
.stat-tile--sev-ok::before,
.stat-tile--sev-warn::before,
.stat-tile--sev-crit::before,
.stat-tile--sev-neutral::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--sp-0_5, 3px);
}
.stat-tile--sev-ok::before {
  background: var(--data-sign-up, var(--sys-green-500));
}
.stat-tile--sev-warn::before {
  background: var(--sys-orangeyellow-500);
}
.stat-tile--sev-crit::before {
  background: var(--data-sign-down, var(--sys-red-500));
}
.stat-tile--sev-neutral::before {
  background: var(--border);
}

}

/* ── ui-kit.components: src/styles/components/metric-meter.css ── */
@layer ui-kit.components {
/* ── MetricMeter ───────────────────────────────────────────────────
   Canonical chrome for the MetricMeter primitive
   (`packages/ui-kit/src/react/components/MetricMeter.tsx`).  Three
   layers: an optional `header` row carrying label + value display, the
   `track` (the empty bar / line), and the `fill` (the value/max-scaled
   bar segment).

   Default dimensions are pinned in the TSX (48 px × 6 px horizontal,
   6 px × 48 px vertical) so the meter slot can sit inline beside a
   stat tile or rank-row metric column without disrupting the row's
   vertical rhythm. */

.metric-meter {
  display: inline-flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.metric-meter__header {
  display: inline-flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  min-width: 0;
}

.metric-meter__label {
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  /* Canonical eyebrow anchor \u2014 see `theme-extras.css \u00a7 Eyebrow
     color anchor`.  Brighter in dark/dim, darker in light than the
     prior `--text-muted` reading so the meter label sits at the
     same contrast rung as `PartitionHeading` + `.stat-tile__label`
     across the whole eyebrow family. */
  color: var(--text-eyebrow);
  /* One rung lighter than the prior inherited body weight (typically
     `--fw-semibold` from the h-tag default the meter sits beside).
     Matches the `PartitionHeading` retune so the canonical eyebrow
     family paints at one consistent weight. */
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
}

.metric-meter__value {
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* Track + fill share full border-radius so the fill never paints
   outside the track's rounded ends, even at 100% ratio. */
.metric-meter__track {
  background: var(--surface-active, var(--surface-tint-2));
  border-radius: var(--shape-full);
  overflow: hidden;
  flex-shrink: 0;
  display: block;
}

.metric-meter__fill {
  border-radius: var(--shape-full);
  /* Width / height are written by the inline style; this transition
     animates the bar growing/shrinking on every prop change. */
  transition: width var(--dur-short-2, 200ms) var(--ease-standard, ease),
              height var(--dur-short-2, 200ms) var(--ease-standard, ease),
              background var(--dur-short-2, 200ms) var(--ease-standard, ease);
}

/* Vertical orientation flips the flex direction of the track wrapper
   so the fill grows from the bottom up (anchoring at align-items:
   flex-end). */
.metric-meter--vertical .metric-meter__track {
  display: flex;
  align-items: flex-end;
}

}

/* ── ui-kit.components: src/styles/components/funnel.css ── */
@layer ui-kit.components {
/* ── Funnel ──────────────────────────────────────────────────────────
   Chrome for the Funnel primitive
   (`packages/ui-kit/src/react/components/Funnel.tsx`).  A horizontal row
   of stage cells separated by annotated connectors, on one outlined
   surface.  Token-only per the kit CSS contract (no hex / rgba / literal
   border-radius). */

.funnel {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  border: var(--border-stroke) solid var(--border);
  border-radius: var(--shape-md);
  background: var(--surface);
  overflow: hidden;
}

.funnel__stage {
  flex: 1 1 120px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-4);
}

.funnel__label {
  font-size: var(--fs-label-md);
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
  color: var(--text-muted);
}

.funnel__value {
  font-size: var(--fs-headline-md);
  line-height: var(--lh-headline-md);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.funnel__sub {
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}

.funnel__connector {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  padding-inline: var(--sp-2);
}

.funnel__arrow {
  color: var(--text-muted);
}

.funnel__delta {
  font-size: var(--fs-body-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-muted);
}

.funnel__delta--up {
  color: var(--sys-green-500);
}

.funnel__delta--down {
  color: var(--sys-red-500);
}

.funnel__delta--neutral {
  color: var(--text-muted);
}

}

/* ── ui-kit.components: src/styles/components/status-banner.css ── */
@layer ui-kit.components {
/* ── StatusBanner ────────────────────────────────────────────────────
   Chrome for the StatusBanner primitive
   (`packages/ui-kit/src/react/components/StatusBanner.tsx`).  A one-line
   system verdict strip: leading Indicator + message/detail stack + a
   trailing verdict slot, on a faintly tone-washed outlined surface with
   a leading severity accent rail.  Token-only per the kit CSS contract
   (no hex / rgba / literal border-radius). */

.status-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: var(--border-stroke) solid var(--border);
  border-radius: var(--shape-md);
  background: var(--surface);
  position: relative;
  overflow: hidden;
}

/* Leading accent rail — mirrors the StatTile severity stripe. */
.status-banner::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--sp-0_5, 3px);
  background: var(--border);
}

.status-banner__lead {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.status-banner__text {
  display: flex;
  flex-direction: column;
  gap: var(--sp-0_5);
  min-width: 0;
}

.status-banner__trailing {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-shrink: 0;
}

/* ── tone: leading rail + faint surface wash ── */
.status-banner--ok::before {
  background: var(--sys-green-500);
}
.status-banner--ok {
  background: var(--sys-green-500-12);
}
.status-banner--warn::before {
  background: var(--sys-orangeyellow-500);
}
.status-banner--warn {
  background: var(--sys-orangeyellow-500-12);
}
.status-banner--crit::before {
  background: var(--sys-red-500);
}
.status-banner--crit {
  background: var(--sys-red-500-12);
}
.status-banner--info::before {
  background: var(--accent);
}
.status-banner--neutral::before {
  background: var(--border);
}

}

/* ── ui-kit.components: src/styles/components/recency-bar.css ── */
@layer ui-kit.components {
/* ── RecencyBar ──────────────────────────────────────────────────────
   Chrome for the RecencyBar primitive
   (`packages/ui-kit/src/react/components/RecencyBar.tsx`).  A short heat
   track whose fill length + color decay with age, plus a relative-time
   caption.  Token-only per the kit CSS contract (no hex / rgba / literal
   border-radius). */

.recency-bar {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.recency-bar__track {
  width: 38px;
  height: 5px;
  border-radius: var(--shape-full);
  background: var(--surface-tint-2);
  overflow: hidden;
  flex: 0 0 auto;
}

.recency-bar__fill {
  display: block;
  height: 100%;
  border-radius: var(--shape-full);
  background: var(--text-muted);
}

.recency-bar__label {
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Age states — fill color decays fresh → stale → dead. */
.recency-bar--fresh .recency-bar__fill {
  background: var(--sys-green-500);
}
.recency-bar--stale .recency-bar__fill {
  background: var(--sys-orangeyellow-500);
}
.recency-bar--dead .recency-bar__fill {
  background: var(--border);
}

.recency-bar--empty {
  color: var(--text-muted);
}

}

/* ── ui-kit.components: src/styles/components/rank-row.css ── */
@layer ui-kit.components {
/* ── RankRow ─────────────────────────────────────────────────────────
   Canonical chrome for the RankRow primitive
   (`packages/ui-kit/src/react/components/RankRow.tsx`).  Mirrors the
   layout / chrome of the existing `.net-rank-row` family but ships
   from the kit so every consumer (Top Meshes, Observers, Loudest, RF
   Links, Long Hops on `/network`, plus any future leaderboard) reads
   from a single source of truth.

   Layout: 2-column grid (`identity` 1fr, `meters` auto), symmetric
   `sp-2` inline padding, `sp-5` block padding so the content block
   breathes comfortably above + below against the row's rounded chrome.
   The block padding is TWO canonical steps taller than the inline
   padding on purpose (20 px vs 8 px) — leaderboard rows read as
   vertically generous "cards" rather than horizontally cramped strips,
   mirroring the ObserverCard / Hero StatTile rhythm in the rest of
   the kit, while staying one notch tighter than the previous sp-6
   ladder so 20 stacked top-N rows pack denser on a single viewport.
   Asymmetric breakpoints tighten the block padding further (short
   variant + mobile) without disturbing the inline rhythm. */

.rank-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--sp-0_5) var(--sp-3);
  /* Block padding (sp-5 = 20 px) intentionally exceeds inline padding
     (sp-2 = 8 px) by 2 steps so the content block sits with
     comfortable breathing room above + below against the row's
     rounded chrome.  Tunes the row's optical height directly; tune
     here to move every consumer (Top Meshes, Observers, Loudest,
     RF Links, Long Hops) in lockstep.  One notch tighter than the
     previous sp-6 default so 20 stacked top-N rows pack denser on a
     single viewport without giving up the card-surface read. */
  padding: var(--sp-5) var(--sp-2);
  text-decoration: none;
  color: inherit;
  /* CHROME contract — the rank row inherits its FULL cassette chrome
     from the kit's `.m3-card` recipe (the `<RankRow>` React component
     composes `m3-card` alongside `rank-row` on the host element).  The
     foundational 1-1-1 px concentric edge construction (outer stroke
     → inner ring → catchlight) AND the active-shimmer compositing
     contract come along for free, no duplication of the cassette
     knobs / bg stack / pseudos here.

     `.rank-row` keeps only the LAYOUT contract (grid template, padding,
     row gap, link reset).  The hover recipe below (.rank-row--interactive:
     hover) mirrors `.m3-card--clickable:hover` so every interactive
     surface in the kit speaks the same brighten-and-lift hover
     language.  When you tune the cassette knobs in `card.css`, every
     rank row across every consumer moves in lockstep. */
  /* Transition pulled forward to add `transform` for the hover lift
     and `border-color` for the shimmer-tone preview ring — the kit's
     `.m3-card` transition only covers `background` + `box-shadow`. */
  transition:
    background var(--dur-short-3, 250ms) var(--ease-standard, ease),
    border-color var(--dur-short-2, 200ms) var(--ease-standard, ease),
    box-shadow var(--dur-short-3, 250ms) var(--ease-standard, ease),
    transform var(--dur-short-2, 200ms) var(--ease-standard, ease);
  /* Canonical row-stack gap — single optical Y distance used inside the
     identity column (name / detail) and the meters rail.  Tune once,
     propagate everywhere. */
  --rank-stack-gap: var(--sp-2);
}

/* Short variant tightens the block padding one step (sp-5 → sp-4,
   20 px → 16 px) so the row still reads as a padded card surface
   but stacks slightly denser for ranks 6–20 below the hero top-5
   strip.  Inline padding stays at sp-2 — the asymmetric rhythm is
   preserved.  Tracks the base row's sp-5 default one notch down,
   matching the canonical density-tier-down ladder elsewhere on the
   row (rank-prefix slot 32 px → 24 px, sub-badges fs-label-sm →
   fs-label-xs). */
.rank-row--short {
  padding-block: var(--sp-4);
}

/* Interactive variants — hover recipe + focus ring + pointer cursor.
   The hover is a single cue: the rim mutates to `--sys-bluepurple-400`
   — the same hue family the `.active-shimmer` comet sweeps through at
   its peak, so hover reads as a static preview of what `active={true}`
   will feel like before the operator clicks.  The active state still
   paints the live conic-rotating + breathing comet shimmer via
   `.active-shimmer` (see `active-shimmer.css`); hover is just the
   colour companion, with no rotation or breathing animation of its own.

   Outline-only by design: the earlier LIFT (`translateY`), FILL
   brighten (`--bg-raised`), and GLOW halo (`--sys-bluepurple-500`)
   cues were removed so the row stays put and only its outline lights
   up — a quieter, more precise hover across a dense 20-row stack. */
.rank-row--interactive {
  cursor: pointer;
}

.rank-row--interactive:hover {
  /* Outline-only hover: the sole cue is the rim mutating to
     `--sys-bluepurple-400` — the same hue family the `.active-shimmer`
     conic-gradient sweeps through at its peak, so hover → active reads
     as one colour language.  The 0.5 px transparent `--container-border`
     goes opaque bluepurple, painting over the resting `--container-bevel`
     rim in the border box.  Deliberately NO lift, NO fill brighten, and
     NO glow halo — those cues were removed so the row stays put and
     only its outline lights up. */
  border-color: var(--sys-bluepurple-400);
}

.rank-row--interactive:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Active / selected state — painted by the `<ActiveShimmer>` component
   (an absolute SVG overlay).  The `active` prop on `<RankRow>` drops
   the component into the row's DOM tree automatically.  The old
   `.rank-row--active` outline is removed to prevent double-stroking. */

/* ── Identity column ── */
.rank-row__identity {
  display: flex;
  flex-direction: column;
  gap: var(--rank-stack-gap);
  min-width: 0;
}

.rank-row__name {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-semibold);
  color: var(--text);
}

/* Rank prefix slot — fixed-width hanging indent so every row's
   identity content aligns regardless of single-digit / double-digit
   rank.  `tabular-nums` for the numeric prefix; the Crown SVG slot
   inherits the same fixed width.

   Typography contract:
     - DM Mono (`--mono`) so the digit + dot sit on the mono cap
       height matrix and align with the row's body-lg name to the
       right at the typographic baseline.
     - `--fw-regular` (400) — thinnest weight on the kit's font-weight
       ladder; DM Mono itself caps at 500 (medium), so 400 reads as
       a notably thinner companion to the name's semibold (600) text.
     - `--text-tertiary` — one rung quieter than `--text-muted` while
       still passing WCAG AA; routes through the semantic token so
       the dim / dark / light themes each resolve the passing zinc step
       (zinc-400 dark/dim, zinc-600 light) rather than the hardcoded
       zinc-500 which fails at 3.67:1 on the dim surface background. */
.rank-row__rank-prefix {
  display: inline-flex;
  align-items: center;
  /* Center the prefix content (numeric digit for ranks 2–20, OR
     Crown SVG for rank 1) inside the slot.  Single canonical rule
     so all 20 rows share one centered-ordinal alignment regardless
     of whether the prefix is text or icon — the column reads as
     a deliberate numbered list with the rank marker anchored to
     each row's slot midline.  Rank 1's Crown SVG inherits this
     centering as a peer of the digits beneath it. */
  justify-content: center;
  /* 32 px slot — wide enough that the centered prefix (label-md
     digit, ~14 px including the period, OR the 13 px-tall Crown
     SVG) drifts visually toward the midpoint between the row's
     left card-edge and the start of the name text.  Previous
     24 px slot kept the prefix hugging the row's leading inset;
     widening to 32 px adds ~4 px of optical breathing room on each
     side so the ordinal reads as a deliberate centered marker
     rather than a left-hung tag.  Hanging-indent rules below
     (`.rank-row__sub-badges`, `.rank-row__detail`, mobile reflow)
     track this width one-to-one so sub-badges + detail content
     keep aligning under the start of the name. */
  min-width: 32px;
  /* Step the ordinal down to label-md (12 px / 16 px lh) so the digit
     + dot read as supporting metadata next to the body-lg semibold
     name text to the right.  Rank-1's Crown SVG (13 px tall) ignores
     this rule — only ranks 2–20 paint a numeric prefix — so the
     glyph still anchors the row at its canonical size. */
  font-size: var(--fs-label-md);
  line-height: var(--lh-label-md);
  letter-spacing: var(--ls-label-md);
  font-variant-numeric: tabular-nums;
  font-family: var(--mono);
  font-weight: var(--fw-regular);
  color: var(--text-tertiary);
  flex-shrink: 0;
}

/* Short variant slot — narrower than the default so the compact
   tier-down also tightens the rank gutter.  Steps the slot width
   from 32 px → 24 px (one canonical sp-2 step) so the ratio between
   default and short slot widths mirrors the canonical block-padding
   ladder elsewhere on the row.  Centering carries through from
   the base rule above — both numeric digit + Crown SVG sit on the
   short slot's midline at this density tier. */
.rank-row--short .rank-row__rank-prefix {
  min-width: 24px;
}

/* Identity content slot — flex-row so consumers can stack the name
   text + IATA chip + RF channel chip + save button inline at body-lg
   typography.  `min-width: 0` enables truncation downstream. */
.rank-row__identity-content {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  flex: 1 1 auto;
}

/* Sub-badge row — inline-flex chips (region / RF channel / state) at
   label-sm muted, hangs under the identity content (indented past the
   rank-prefix hanging-indent column).  Wraps at narrow widths so chips
   never push the meters rail off-screen.  In `short` variant, the chip
   strip stays visible but tightens its font-size + gap. */
.rank-row__sub-badges {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  /* 32 px tracks the default `.rank-row__rank-prefix` slot width so
     the chip strip indents under the start of the name (not under
     the rank digit).  Keep in sync with the slot's `min-width`. */
  margin-inline-start: calc(32px + var(--sp-2));
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  letter-spacing: var(--ls-label-sm);
  color: var(--text-muted);
  min-width: 0;
}

.rank-row--short .rank-row__sub-badges {
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  gap: var(--sp-0_5);
}

/* Detail sub-line — label-sm muted, indents under the identity
   content (skips the rank-prefix hanging-indent column). */
.rank-row__detail {
  display: block;
  /* Tracks `.rank-row__rank-prefix` slot width (32 px default).
     Keep in sync with the slot's `min-width`. */
  margin-inline-start: calc(32px + var(--sp-2));
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  letter-spacing: var(--ls-label-sm);
  color: var(--text-muted);
  font-family: var(--mono);
}

.rank-row--short .rank-row__detail {
  display: none;
}

/* ── Meters column ── */
.rank-row__meters {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  flex-shrink: 0;
}

/* ── Leading media panel (optional 3rd column) ────────────────────
   Activated by the `<RankRow leadingMedia>` prop.  Mirrors the kit's
   ObserverCard recipe (`Components/Maps/StaticMiniMap/InsideObserverCard`):
   a bounded panel at the row's leading edge that hosts a per-row
   visual (`StaticMiniMap`, `StaticRouteMap`, sparkline, identicon)
   flush against the row's rounded chrome on its outer side.

   Layout shift when `--with-media` is in play:
     - Grid template grows to 3 columns: `[media] [identity] [meters]`
     - The row strips its block padding + leading inline padding so the
       media panel can bleed to the rounded chrome on its outer edge.
       Inner column inset is restored on `__identity` + `__meters`
       directly so the canonical vertical rhythm survives.
     - `overflow: hidden` ensures the panel clips cleanly to the row's
       `--shape-lg` outer radius (the `<StaticMiniMap>` canvas would
       otherwise paint past the rounded corners).
     - Minimum height bumps to `--sp-16` so a mini-map has enough
       vertical real estate to read as geography rather than a strip. */
.rank-row--with-media {
  grid-template-columns: auto minmax(0, 1fr) auto;
  column-gap: var(--sp-3);
  padding-block: 0;
  padding-inline-start: 0;
  overflow: hidden;
  /* Min-height 180 px — matches the 16:10 panel height at the
     canonical 288 px width below.  Stays as `min-height` so a row
     whose identity / meters columns grow taller than the map (e.g.
     a 7-hop subBadges chain in the long-hop section) can still
     expand past 180 px without leaving an empty gap below the map.
     The aspect-ratio declaration on `.rank-row__media` always wins
     over the row's stretch so the map itself stays 16:10 regardless. */
  min-height: 180px;
}
.rank-row__media {
  /* Canonical 16:10 panel — 288 × 180.  `aspect-ratio: 16 / 10`
     pins the panel's height to its width so the cartography always
     reads as a wide-format map surface rather than a square thumbnail
     or a vertical strip.  16:10 (rather than 4:3 → 240×180 the
     previous incarnation used) gives the rendered geography more
     horizontal canvas without bumping the row height — the section's
     hero rows feel more like cartographic surfaces and less like
     icon panels.

     Why `aspect-ratio` instead of an explicit `height`?  The kit's
     row primitive carries `min-height: 180 px` on the `.rank-row--with-
     media` modifier (see above), so an explicit height would either
     conflict (when content forces the row taller) or leave the map
     letterboxed (when the row stays at min).  Aspect-ratio derives
     the panel's intrinsic size from its width, then the panel sits
     at the TOP of the cell while the surrounding identity / meters
     columns flex freely — row + map stay independent.  Pairs with
     `align-self: start` to lock the panel to the row's top edge
     rather than centering inside a taller content cell.

     288 px width is wide enough to host the `InsideObserverCard`
     recipe's signal-colored edges + endpoint halos at a legible
     zoom on the typical §5 / §6 column (each column sits between
     ~320 px and ~640 px wide depending on viewport), while still
     leaving room for endpoint tags / hop chains / RF channel chips
     / OBS badges in the identity column beside the panel. */
  width: 288px;
  aspect-ratio: 16 / 10;
  align-self: start;
  position: relative;
  overflow: hidden;
  /* Background tint while the GL context is mounting / context-loss
     evicted the canvas — same `--surface-sunken` token the
     ObserverCard story paints behind its `<StaticMiniMap>` so the
     panel reads as a quiet sunken slot instead of a transparent gap. */
  background: var(--surface-sunken);
}
/* Re-apply the standard block padding to the content columns only so
   the canonical vertical rhythm survives the row-level
   `padding-block: 0` strip above.  Tracks the row-level
   `padding-block` ladder one-to-one: default sp-5, short sp-4 — a
   tune to the row-level rule propagates here in lockstep. */
.rank-row--with-media > .rank-row__identity,
.rank-row--with-media > .rank-row__meters {
  padding-block: var(--sp-5);
}
.rank-row--with-media.rank-row--short > .rank-row__identity,
.rank-row--with-media.rank-row--short > .rank-row__meters {
  padding-block: var(--sp-4);
}
/* Restore the row's right-edge inset on the meters column so the
   metric rail keeps its breathing room against the rounded chrome
   (the row-level `padding-inline-end` still applies, but stating
   it here keeps the rule explicit for future tuning). */
.rank-row--with-media > .rank-row__meters {
  padding-inline-end: var(--sp-2);
}

/* ── Mobile responsive collapse (≤ 599 px) ───────────────────────
   Ports the legacy `.net-rank-row` mobile reflow from
   `frontend/src/styles/pages/network-stats.css` so the kit primitive
   carries its own responsive contract instead of relying on the
   consumer page sheet's media queries (which target the retired
   `.net-rank-*` class family and don't fire for `<RankRow>`-rendered
   leaderboards).  Without this block the desktop 2-column grid stays
   at narrow widths, the meters rail consumes most of the row, and the
   identity column's sub-badges / detail line wedge underneath each
   other — reads as visual "collapse".

   Collapse shape:
     col:   [1fr]                          [auto]
     row 1: name (spans both cols)
     row 2: sub-badges (col 1)             | meters (col 2, rows 2→-1)
     row 3: detail (col 1)                 | (continues)

   The identity column dissolves into `display: contents` so each of
   its children (`__name`, `__sub-badges`, `__detail`) becomes a direct
   grid child and can claim its own row.  Meters spans rows 2 to -1 so
   the metric rail packs flush against the right edge while the
   identity stack reads down the left.  Hanging indent (rank prefix
   width + sp-2) is preserved on sub-badges + detail via
   `padding-inline-start` instead of `margin-inline-start` so the
   indent contract survives the grid-item placement. */
@media (max-width: 599px) {
  .rank-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto auto;
    align-items: start;
    column-gap: var(--sp-2);
    row-gap: var(--sp-1);
    padding: var(--sp-2);
  }

  /* Media panel dissolves on phones — a 180 px bounded panel inside a
     ~360 px row would crowd the identity column past the point of
     usefulness, and iOS Safari's GL context budget can't afford a map
     instance for every rank row in a tightly-stacked list.  Hiding
     the panel also reverts the grid template to the canonical 2-column
     mobile reflow declared above so `__sub-badges` / `__detail` /
     `__meters` slot back into their canonical mobile rows. */
  .rank-row__media {
    display: none;
  }
  .rank-row--with-media {
    grid-template-columns: minmax(0, 1fr) auto;
    padding-block: var(--sp-2);
    padding-inline-start: var(--sp-2);
    /* Drop the desktop min-height — the row should size to its content
       on phones, same as every other rank row. */
    min-height: 0;
  }

  /* Identity dissolves into the outer grid so its children become
     direct grid items and can claim individual rows. */
  .rank-row__identity {
    display: contents;
  }

  /* Name claims row 1 across both columns so a long node name has the
     full row width to truncate cleanly. */
  .rank-row__name {
    grid-column: 1 / -1;
    grid-row: 1;
    min-width: 0;
  }

  /* Sub-badges drop to row 2 col 1.  Swap `margin-inline-start` for
     `padding-inline-start` so the hanging indent doesn't subtract from
     the column's flex basis (margin on a grid item shrinks the box;
     padding leaves the item's width intact).  32 px tracks the
     default `.rank-row__rank-prefix` slot width — keep in sync. */
  .rank-row__sub-badges {
    grid-column: 1;
    grid-row: 2;
    margin-inline-start: 0;
    padding-inline-start: calc(32px + var(--sp-2));
    flex-wrap: nowrap;
    overflow: hidden;
  }

  /* Detail drops to row 3 col 1.  Tracks the default slot width. */
  .rank-row__detail {
    grid-column: 1;
    grid-row: 3;
    margin-inline-start: 0;
    padding-inline-start: calc(32px + var(--sp-2));
  }

  /* Meters claims col 2 rows 2 → -1 so the metric rail packs vertically
     against the row's right edge alongside the identity stack on the
     left. */
  .rank-row__meters {
    grid-column: 2;
    grid-row: 2 / -1;
    align-self: stretch;
    align-items: flex-end;
  }

  /* Short variant tightens further — detail row hides (matches desktop
     `.rank-row--short .rank-row__detail { display: none }`) and the
     grid collapses to 2 rows. */
  .rank-row--short {
    grid-template-rows: auto auto;
    padding: var(--sp-1) var(--sp-2);
  }
}

}

/* ── ui-kit.components: src/styles/components/tag-kinds.css ── */
@layer ui-kit.components {
/* ── tag-kinds.css — domain-token tonal recipes for `<Tag kind="...">`.
   Folds the legacy `.pkt-badge--*` family from `live-packets.css` into
   the canonical `<Tag>` primitive.  Each `.m3-tag--{kind}` rule paints
   background + foreground from the kit's wheel-family anchor stops so
   the domain palette stays in lockstep with the chart palette.

   These rules land in the `ui-kit.components` layer AFTER the
   canonical M3 tone block in `m3-controls.css`, so per-kind rules
   automatically win when a consumer passes both `tone` and `kind`.
   (`Tag.tsx` also skips emitting the `m3-tag--{tone}` class when
   `kind` is set, keeping the class list lean.)

   Truncate modifier mirrors the retired PacketBadge contract — clamps
   long firmware / hash strings with `text-overflow: ellipsis`.  Pair
   with a `title` so the full value remains recoverable on hover. */

/* Truncation modifier — pair with `title` for hover-recoverable
   value strings (firmware version, long hash, etc.). */
.m3-tag--truncate {
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline-block;
  vertical-align: middle;
  padding-block: 3px;
}

.m3-tag--lg.m3-tag--truncate {
  padding-block: var(--sp-1_5);
}

/* ── Per-context tonal recipes ──
   Region / model / firmware / client identity tokens. */

.m3-tag--region {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}

/* ── `node-label` ── CANONICAL system-wide recipe for node-identity
   chips, sourced one-for-one from `createNodeLabelImage` in
   `frontend/src/components/map/map-images.ts` (the function that
   rasterises every observer / repeater / companion / node identity
   pill on the live MapLibre map).  Mirrors that canvas recipe into a
   CSS-rendered Tag so a node-identity chip on the map and the same
   node's chip in the mover feed / Top-20 row / story ticker / packet
   detail modal read as ONE family across both rasterised + DOM
   surfaces.

   The kit's `<NodeTag>` wrapper is the reach-for-this primitive ANY
   time a node identity is rendered as a chip — see
   `packages/ui-kit/src/react/components/NodeTag.tsx`.  Hand-rolling a
   `<Tag kind="node-label">` still works (the wrapper is sugar over
   exactly that), but `<NodeTag name="\u2026" nodeKind="observer">` is
   discoverable, ties to the canonical node-kind taxonomy, and routes
   through the per-kind modifier slot below without a hand-composed
   className.

   Recipe (verbatim from `map-images.ts`):
     dark:   text `#cdd4f6` (canvas-tuned brand-light) on a 70%-alpha
             black chip with a 12%-alpha white 1 px stroke.
     light:  text `#232658` (canvas-tuned brand-deep) on a 33%-alpha
             white chip with a 10%-alpha black 1 px stroke.

   Chip chrome:
     border-radius `--shape-sm` (currently 4 px under the temporary global
       flatten). The MapLibre canvas recipe still uses LABEL_CHIP_RADIUS=8;
       the DOM radius follows the kit token rather than claiming pixel parity.
     asymmetric padding-block — slightly more on the bottom to mirror
       the canvas's `LABEL_PAD_Y_BOTTOM_EXTRA` nudge so the text sits
       optically centred at every size tier.
     `backdrop-filter: var(--blur-sm)` — essential in the DOM context.
       The canvas chip's 70%-alpha black reads against map tiles; in
       the DOM, chips sit on opaque parent surfaces (zebra stripes,
       hover wash, card bg), so the blur turns the translucency into
       a frosted-glass continuation of the page beneath rather than a
       flat-tinted pill.

   Specificity contract: the `.m3-tag.m3-tag--node-label` chained
   selector (0,2,0) ties / beats `.m3-tag--lg.m3-tag--truncate` (0,2,0)
   in the cascade so the node-label chrome owns its padding-block /
   border-radius / height even when composed alongside the kit's
   truncate modifier.  Size-tier overrides bump to (0,3,0) so they win
   over both the base recipe and truncate. */
.m3-tag.m3-tag--node-label {
  /* Closely follows `createNodeLabelImage` in `map-images.ts` for typography,
     padding, colour, and ring placement. Radius is the deliberate exception:
     the DOM uses the semantic kit token, currently flattened to 4 px, while
     the canvas keeps its literal 8 px LABEL_CHIP_RADIUS. Retune both surfaces
     deliberately rather than assuming they are pixel-identical.

     Canvas constants → CSS:
       LABEL_FONT_WEIGHT  500  →  font-weight: var(--fw-medium)
       LABEL_FONT_SIZE_MD 11   →  font-size:   var(--fs-label-sm)
       LABEL_PAD_X        9    →  padding-inline: 9px
       LABEL_PAD_Y        5    →  padding-block-start: 5px
       LABEL_PAD_Y_BOTTOM_EXTRA 1.333 → padding-block-end:    6px
                                       (rounded; subpixel padding
                                       wouldn't survive layout anyway)
       LABEL_STROKE_WIDTH 1    →  ::before pseudo ring (see below)
       shape role         small → border-radius: var(--shape-sm)

     Note on `border` removal: the chip's solid `border` was tried
     first but it positions the ring at the BORDER-BOX edge for solid
     colours yet the gradient (`border-image`) doesn't follow rounded
     corners.  The observer-variant pseudo workaround then placed the
     gradient ring 1 px inside the chip's outer edge (at the padding
     box) while the node variant's solid border stayed at the outer
     edge — two ring positions, inconsistent across variants.  And the
     `m3-tag--truncate` modifier's `overflow: hidden` cropped a pseudo
     positioned at `inset: -1px` (extending past padding-box into the
     border area).

     Resolution: drop the chip's own border entirely, bump padding by
     1 px on each side to preserve the canvas-port dimensions, and
     paint the ring via a SINGLE `::before` pseudo at `inset: 0` for
     EVERY variant.  The pseudo's outer edge lands at the chip's
     padding-box outer (= border-box outer with `border: 0`), which
     coincides with the `overflow: hidden` clip boundary, so the ring
     is never cropped — it paints right at the chip's visible outer
     edge across every variant + parent context. */
  height: auto;
  border-radius: var(--shape-sm);
  padding: 6px 10px 7px;
  /* No own border — the unified `::before` pseudo paints the ring. */
  border: 0;
  /* Position relative to anchor the `::before` ring pseudo below. */
  position: relative;
  font-family: var(--font);
  font-weight: var(--fw-medium);
  font-size: var(--fs-label-sm);
  /* Match the canvas's tight cap-height rendering — native canvas text
     has no CSS line-leading, so the DOM chip pins `line-height: 1` to
     achieve the same pixel-precise glyph stack. */
  line-height: 1;
  letter-spacing: var(--ls-label-sm);
  /* Frosted-glass backdrop — the canvas chip's 70%-alpha black reads
     against map tiles; in the DOM, chips sit on opaque parent surfaces
     (zebra stripes, hover wash, card bg), so the blur turns the
     translucency into a frosted continuation of the page beneath
     instead of a flat-tinted pill. */
  backdrop-filter: var(--blur-sm);
  -webkit-backdrop-filter: var(--blur-sm);
  /* Dark-theme defaults — literal rgba / hex per the canvas recipe.
     Tokenising these would require a per-theme node-label-{chip,fg,
     stroke} fork in the palette; the canvas + DOM surfaces share the
     same one-knob retune point at `map-images.ts` LABEL_THEME + this
     rule pair, which is the design contract today. */
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- canvas-verbatim chip alpha; retune at LABEL_THEME.dark.chip in map-images.ts */
  background: rgba(0, 0, 0, 0.70);
  /* stylelint-disable-next-line color-no-hex -- canvas-verbatim text colour; retune at LABEL_THEME.dark.text in map-images.ts */
  color: #cdd4f6;
}

/* Unified ring pseudo — paints the 1 px keyline for EVERY node-label
   variant at the SAME position (chip outer edge).  Default background
   is the solid 24 %-alpha white (DOM-context bump above the canvas's
   12 % verbatim); observer overrides this to the brand-anchor gradient
   below; light theme overrides this to the matching black tone.

   Consumer override: the `background` value reads through the
   `--node-tag-ring` CSS custom property FIRST, with the canonical
   white-24 stroke as the fallback.  `<NodeTag ringColor="\u2026">` sets
   that custom property via inline style on the chip; the per-variant
   rules below (observer gradient, light-theme black) ALSO read
   through the same custom property so a consumer override wins over
   per-variant defaults across every theme.  Drop the prop \u2192 each
   rule falls back to its per-variant default, preserving the canvas
   contract.  See `NodeTag.tsx` for the consumer API.

   Layout: `inset: 0` puts the pseudo's outer edge at the chip's
   padding-box outer (= border-box outer with `border: 0`).  The
   1 px `padding` defines the ring thickness, and the two-layer
   mask + `mask-composite: exclude` punches out the inner content-box,
   leaving only the 1 px padding ring painted.  Stroke alpha tier
   above the canvas verbatim mirrors the dark-theme rationale (see
   header comment on `.m3-tag.m3-tag--node-label` above). */
.m3-tag.m3-tag--node-label::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: var(--stroke-hairline);
  background: var(--node-tag-ring, color-mix(in srgb, white 24%, transparent));
  -webkit-mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  mask-composite: exclude;
  /* The pseudo overlays the chip's text bounds; pass interactions
     through so the chip itself receives hover / click / focus. */
  pointer-events: none;
}

/* Size tiers — the chip box stays at canvas dimensions across every
   tier so the shape reads as the SAME family on every surface.  Only
   the typeface scales one step in each direction, mirroring how the
   canvas would render at higher / lower DPR without changing the
   chip's padding or radius.  Reach for `sm` when a row's line-height
   is tight enough that the canonical 11 px would feel heavy (compact
   ticker, narrow list columns); reach for `lg` only when the chip is
   the row's hero (mover-feed identity column on roomy viewports). */
.m3-tag.m3-tag--sm.m3-tag--node-label {
  font-size: var(--fs-label-xs);
  letter-spacing: var(--ls-label-xs);
}
.m3-tag.m3-tag--lg.m3-tag--node-label {
  font-size: var(--fs-label-md);
  letter-spacing: var(--ls-label-md);
}

/* ── Heroic typescale ladder (`<NodeTag typeScale="\u2026">`) ──
   Heroic / page-anchor rungs that override the canvas-faithful size
   tier above when the chip IS a page hero rather than a row
   decoration.  Each rung pulls font-size + letter-spacing from the
   kit's typescale tokens (matching `<Text size="\u2026">` 1:1) and scales
   padding + border-radius PROPORTIONALLY so the chip grows as a
   unified primitive instead of looking like a small chip stretched
   tall.

   Padding follows the canvas's asymmetric ~0.5/0.55 (top/bottom) ratio
   against font-size, with horizontal padding at ~0.9\u00d7 font-size, so
   each rung carries the SAME optical centring nudge the canvas chip
   bakes in via `LABEL_PAD_Y_BOTTOM_EXTRA`.

   Border-radius is ANCHORED to the canvas reference and scales
   continuously, not in discrete `--shape-*` token jumps.  The canvas
   recipe sets `LABEL_CHIP_RADIUS = 8` against `LABEL_FONT_SIZE_MD = 11`
   (see `createNodeLabelImage` in `frontend/src/components/map/map-
   images.ts`), yielding the canonical radius:font ratio of 8/11
   \u2248 0.727.  Every rung below applies that exact ratio to its
   font-size and rounds to the nearest pixel, so a chip rendered at
   any typescale reads as a proportional scale-up of the same canvas
   pill rather than visually disconnected radius steps. The current semantic
   shape scale is intentionally flat, so these proportional exceptions do not
   map to `--shape-*` values; we emit literal pixel values so the contract
   is one-knob-retunable: a future change to `LABEL_CHIP_RADIUS` or
   `LABEL_FONT_SIZE_MD` recomputes every rung from one constant
   instead of requiring per-rung token swaps.

   Computed radius per rung (font-size \u00d7 8/11, rounded to nearest px):
     label-md     12 px → 9 px         title-lg     22 px → 16 px
     label-lg     14 px → 10 px        headline-sm  24 px → 17 px
     title-sm     14 px → 10 px        headline-md  28 px → 20 px
     title-md     16 px → 12 px        headline-lg  32 px → 23 px
                                       display-sm   36 px → 26 px
                                       display-md   45 px → 33 px

   These selectors chain `.m3-tag.m3-tag--node-label.m3-tag--node-label--type-{rung}`
   (specificity 0,3,0) so they tie the size-tier overrides above
   (also 0,3,0) and WIN by cascade order (typescale rules ship after
   size rules in this file).  See `NodeTag.tsx` for the consumer API
   + per-rung reach-for-this guide.

   `line-height: 1` is inherited from the base recipe — every rung
   stays cap-tight so the chip never carries vertical leading. */
/* stylelint-disable declaration-property-value-disallowed-list -- heroic-typescale radii are intentionally PROPORTIONAL (font-size * 8/11; see per-rung comments), not --shape-* ladder values; snapping them to the ladder would break the proportional-growth contract. */
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-label-md {
  font-size: var(--fs-label-md);
  letter-spacing: var(--ls-label-md);
  padding: 6px 10px 7px;
  /* 12 px \u00d7 8/11 \u2248 8.7 \u2192 9 px */
  border-radius: 9px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-label-lg {
  font-size: var(--fs-label-lg);
  letter-spacing: var(--ls-label-lg);
  padding: 7px 12px 8px;
  /* 14 px \u00d7 8/11 \u2248 10.2 \u2192 10 px */
  border-radius: 10px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-title-sm {
  font-size: var(--fs-title-sm);
  letter-spacing: var(--ls-title-sm);
  padding: 7px 12px 8px;
  /* 14 px \u00d7 8/11 \u2248 10.2 \u2192 10 px */
  border-radius: 10px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-title-md {
  font-size: var(--fs-title-md);
  letter-spacing: var(--ls-title-md);
  padding: 8px 14px 9px;
  /* 16 px \u00d7 8/11 \u2248 11.6 \u2192 12 px */
  border-radius: 12px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-title-lg {
  font-size: var(--fs-title-lg);
  letter-spacing: var(--ls-title-lg);
  padding: 10px 18px 11px;
  /* 22 px \u00d7 8/11 = 16 px */
  border-radius: 16px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-headline-sm {
  font-size: var(--fs-headline-sm);
  letter-spacing: var(--ls-headline-sm);
  padding: 11px 20px 12px;
  /* 24 px \u00d7 8/11 \u2248 17.5 \u2192 17 px */
  border-radius: 17px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-headline-md {
  font-size: var(--fs-headline-md);
  letter-spacing: var(--ls-headline-md);
  padding: 13px 24px 14px;
  /* 28 px \u00d7 8/11 \u2248 20.4 \u2192 20 px */
  border-radius: 20px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-headline-lg {
  font-size: var(--fs-headline-lg);
  letter-spacing: var(--ls-headline-lg);
  padding: 14px 26px 15px;
  /* 32 px \u00d7 8/11 \u2248 23.3 \u2192 23 px */
  border-radius: 23px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-display-sm {
  font-size: var(--fs-display-sm);
  letter-spacing: var(--ls-display-sm);
  padding: 16px 28px 17px;
  /* 36 px \u00d7 8/11 \u2248 26.2 \u2192 26 px */
  border-radius: 26px;
}
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-display-md {
  font-size: var(--fs-display-md);
  letter-spacing: var(--ls-display-md);
  padding: 18px 32px 19px;
  /* 45 px \u00d7 8/11 \u2248 32.7 \u2192 33 px */
  border-radius: 33px;
}
/* stylelint-enable declaration-property-value-disallowed-list */

/* Hero-tier keyline ring bump — the canvas-faithful 1 px stroke
   disappears against the chip body at display-* sizes, so lift the
   ring thickness to 2 px so the keyline reads as a deliberate edge
   at hero scale.  Selector chains the type modifier on the pseudo
   so only display-sm + display-md inherit the lift; headline-* and
   smaller stay at the canvas-verbatim 1 px. */
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-display-sm::before,
.m3-tag.m3-tag--node-label.m3-tag--node-label--type-display-md::before {
  padding: 2px;
}

/* Light-theme pivot — the canvas's LABEL_THEME.light triple lifted
   into the kit's canonical `:root[data-theme="light"]` selector.  Same
   stroke-alpha bump as the dark-theme branch above (→ 20 % from the
   canvas's 10 % verbatim) so the keyline reads as a clear ring against
   the paper-toned light card surface. */
:root[data-theme="light"] .m3-tag.m3-tag--node-label {
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- canvas-verbatim chip alpha; retune at LABEL_THEME.light.chip in map-images.ts */
  background: rgba(255, 255, 255, 0.33);
  /* stylelint-disable-next-line color-no-hex -- canvas-verbatim text colour; retune at LABEL_THEME.light.text in map-images.ts */
  color: #232658;
}
:root[data-theme="light"] .m3-tag.m3-tag--node-label::before {
  /* Light-theme ring \u2014 same `--node-tag-ring` consumer-override hook
     as the base rule above; falls back to the canvas-verbatim 20 %
     black tone for the paper-toned light card surface.  Keeping the
     custom-property route here means `<NodeTag ringColor="\u2026">` wins
     in BOTH themes without forcing the consumer to also override the
     `data-theme="light"` variant. */
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- DOM-context stroke alpha bump (canvas-verbatim is 10 %); the ring reads clearly against the paper-toned light card surface at 20 %. */
  background: var(--node-tag-ring, rgba(0, 0, 0, 0.20));
}

/* ── Per-node-kind modifiers ──
   Each modifier composes alongside the base `node-label` recipe to
   carry node-kind-specific chrome.  Observer's gradient stroke is
   the active distinction today (matches the canvas observer-pin
   gradient); the remaining kinds (companion / repeater / roomServer
   / sensor / node) share the base treatment but get reserved hooks
   here so a future canvas-side per-kind chrome swap (e.g., a tinted
   stroke on repeater chips) lands without touching call sites.

   The taxonomy mirrors `NODE_IMAGE_IDS` in `map-images.ts` so a
   `<NodeTag nodeKind="\u2026">` consumer speaks the same kind vocabulary
   as the live-map pin renderer. */
/* Observer-variant ring — only the pseudo's `background` differs from
   the default; the chip's pseudo at `inset: 0` + mask-composite already
   wins the ring geometry, so this override just paints the brand-anchor
   gradient instead of the solid white-24 default.  Light + dark themes
   share the same gradient — the brand anchors read on both surfaces.

   Consumer-override hook: same `var(--node-tag-ring, \u2026)` pattern as
   the base + light-theme rules \u2014 a caller passing `ringColor` to a
   `<NodeTag nodeKind="observer">` wins over the gradient.  Useful for
   per-row directional semantics (e.g. RF-link endpoint chips) without
   needing to fork the chip body or the observer kind taxonomy. */
.m3-tag.m3-tag--node-label.m3-tag--node-label--observer::before {
  /* stylelint-disable-next-line color-no-hex -- canvas-verbatim brand-stable anchor; retune at createNodeLabelImage observer branch in map-images.ts */
  background: var(--node-tag-ring, linear-gradient(to right, #3f418d, #464ba0));
}
/* Reserved per-kind hooks — share the base treatment today; future
   canvas-side per-kind chrome drops in here without changing call
   sites.  The empty rulesets are intentional (and load-bearing as
   stable selectors NodeTag emits unconditionally so consumers can
   theme by kind without a JS gate). */
.m3-tag.m3-tag--node-label.m3-tag--node-label--repeater,
.m3-tag.m3-tag--node-label.m3-tag--node-label--companion,
.m3-tag.m3-tag--node-label.m3-tag--node-label--roomServer,
.m3-tag.m3-tag--node-label.m3-tag--node-label--sensor,
.m3-tag.m3-tag--node-label.m3-tag--node-label--node {
  /* Inherits the base `.m3-tag.m3-tag--node-label` chrome. */
}

.m3-tag--model,
.m3-tag--firmware,
.m3-tag--client {
  background: var(--surface-tint-2);
  color: var(--text-muted);
  border-color: transparent;
}

/* Origin / direction badges */
.m3-tag--rx {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}

.m3-tag--tx_forward,
.m3-tag--tx_local {
  background: var(--sys-orangeyellow-500-12);
  color: var(--tone-orangeyellow-fg);
  border-color: transparent;
}

/* State / flag badges */
.m3-tag--drop,
.m3-tag--flood {
  background: var(--sys-red-500-12);
  color: var(--tone-red-fg);
  border-color: transparent;
}

.m3-tag--zero-hop,
.m3-tag--signed {
  background: var(--sys-green-500-12);
  color: var(--tone-green-fg);
  border-color: transparent;
}

.m3-tag--observed {
  background: var(--sys-greenblue-500-12);
  color: var(--tone-greenblue-fg);
  border-color: transparent;
}

/* Hash / score / size badges */
.m3-tag--src-hash {
  background: var(--sys-green-500-12);
  color: var(--tone-green-fg);
  border-color: transparent;
}

.m3-tag--dst-hash {
  background: var(--sys-greenblue-500-12);
  color: var(--tone-greenblue-fg);
  border-color: transparent;
}

.m3-tag--path-hash {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}

.m3-tag--score,
.m3-tag--size {
  background: var(--surface-tint-2);
  color: var(--text-tertiary);
  border-color: transparent;
}

/* Repeat-observation badge.  Click affordance (cursor + hover-wash
   transition) is folded in here so the selector isn't declared twice. */
.m3-tag--rpt {
  background: var(--sys-greenblue-500-12);
  color: var(--tone-greenblue-fg);
  border-color: transparent;
  cursor: pointer;
  transition: background var(--dur-short-2) var(--ease-standard);
}

/* ── Route + transport variants ──
   FLOOD / DIRECT / transport_FLOOD / transport_DIRECT.  DIRECT uses
   a neutral zinc tonal so it sits visually below the colored variants. */
.m3-tag--route-flood {
  background: var(--sys-red-500-12);
  color: var(--tone-red-fg);
  border-color: transparent;
}

.m3-tag--route-direct {
  background: color-mix(in srgb, var(--sys-zinc-500) 12%, transparent);
  color: var(--sys-zinc-300);
  border-color: transparent;
}

.m3-tag--route-transport_flood {
  background: var(--sys-orange-500-12);
  color: var(--tone-orange-fg);
  border-color: transparent;
}

.m3-tag--route-transport_direct {
  background: color-mix(in srgb, var(--sys-zinc-500) 12%, transparent);
  color: var(--sys-zinc-400);
  border-color: transparent;
}

/* Light-mode dark-text override for route-direct so the chip stays
   legible against the lighter surface in the light theme. */
:root[data-theme="light"] .m3-tag--route-direct {
  color: var(--sys-zinc-800);
}

/* ── Payload-type kinds ──
   Lift of the `.pkt-badge--{ADVERT|TRACE|ACK|…}` family from
   `live-packets.css`.  Tones stay in lockstep with `chartTypeColors.ts`
   so the per-type chip palette matches the chart series colors.  Generic
   `m3-tag--type` is the fallback when the payload type is unknown. */
.m3-tag--type {
  background: var(--surface-hover);
  color: var(--text);
  border-color: transparent;
}
.m3-tag--ADVERT {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}
.m3-tag--TRACE {
  background: var(--sys-purplered-500-12);
  color: var(--tone-purplered-fg);
  border-color: transparent;
}
.m3-tag--ACK {
  background: var(--sys-yellow-500-12);
  color: var(--tone-yellow-fg);
  border-color: transparent;
}
.m3-tag--PATH {
  background: var(--sys-greenblue-500-12);
  color: var(--tone-greenblue-fg);
  border-color: transparent;
}
.m3-tag--REQ {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}
.m3-tag--RESPONSE {
  background: var(--sys-green-500-12);
  color: var(--tone-green-fg);
  border-color: transparent;
}
.m3-tag--GRP_TXT,
.m3-tag--GRP_DATA {
  background: var(--sys-orangeyellow-500-12);
  color: var(--tone-orangeyellow-fg);
  border-color: transparent;
}
.m3-tag--ANON_REQ {
  background: var(--sys-bluepurple-500-12);
  color: var(--tone-bluepurple-fg);
  border-color: transparent;
}
.m3-tag--TXT_MSG {
  background: var(--sys-greenblue-500-12);
  color: var(--tone-greenblue-fg);
  border-color: transparent;
}
.m3-tag--MULTIPART {
  background: var(--sys-purplered-500-12);
  color: var(--tone-purplered-fg);
  border-color: transparent;
}
.m3-tag--CONTROL,
.m3-tag--RAW_CUSTOM {
  background: var(--sys-red-500-12);
  color: var(--tone-red-fg);
  border-color: transparent;
}

.m3-tag--rpt:hover {
  background: color-mix(in srgb, var(--sys-greenblue-500) 24%, transparent);
}

}

/* ── ui-kit.components: src/styles/components/modules.css ── */
@layer ui-kit.components {
/* ── Unified module recipes: .m3-icon-tile + .card--hover-wash + .card--hover-lift ─ */

/* ═══ Unified module recipes ═══════════════════════════════════════════
   Three "default + subtract" modules that extend the `.m3-card`
   contract above with one tile module + two card-hover modifiers.
   Each canonical class is chained against the legacy class that
   historically inlined the recipe, so the rule body lives in ONE
   place and consumers may either keep their legacy class names or
   migrate to `.m3-icon-tile` / `.card--hover-wash` /
   `.card--hover-lift` over time without a TSX edit.  See AGENT.md
   "Unified module catalog" for the migration roadmap. */

/* ── .m3-icon-tile — canonical leading-icon tile ───────────────────────
   Saturated accent (bluepurple) fill + white glyph + `--shape-sm` (currently 4 px) corners +
   `--sp-8` (32 px) square.  Used as the leading-icon slot in
   NodeDetail status / topology rows and any future accent-disclosure
   site.  The color tokens (`--accent`, `--on-primary`) are
   intentionally theme-stable so the tile renders identically across
   dark / dim / light surfaces AND across any nested modal context.
   Subtractors (`--tonal`, `--flat`) strip the saturated fill while
   preserving box size and corner shape.

   Legacy aliases chained in:
     • `.nd-status-icon-tile` — NodeDetail status + topology rows. */
/* Phase 5f: kit owns only `.m3-icon-tile`.  `.nd-status-icon-tile`
   alias opts in via the app-side roster (kit-aliases.css). */
.m3-icon-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-8);
  height: var(--sp-8);
  border-radius: var(--shape-sm);
  /* Canonical filled recipe — literal `#595fb4` (theme-stable
     bluepurple-300, same hex as `--state-filled-bg`) + zinc-100 glyph,
     mirroring `.m3-btn--filled` so every accent icon tile reads at
     the SAME saturated-on-bright pair as the M3 button roster across
     dark / dim / light.  Replaces `var(--accent)` which pivots to
     `-700` in light per the palette pivot; the brand-stable token
     resolves to the canonical bluepurple-300 hex theme-stably. */
  background: var(--brand-stable-300);
  color: var(--sys-zinc-100);
  flex-shrink: 0;
}
/* Force-bind the descendant glyph color so SVGs inside the tile read
   on the saturated fill regardless of any per-class color rule that
   would otherwise win the cascade.  Pinned to theme-stable zinc-100
   so the glyph matches the bluepurple-300 filled-button family
   across dark / dim / light. */
.m3-icon-tile svg {
  color: var(--sys-zinc-100);
}

/* Tonal subtractor — accent-tonal background (`--glow` = bluepurple-500
   @ 15%) + accent glyph.  Reads as a subtler "primed" accent without
   the saturated commitment.  Use when the parent surface already
   carries chrome and the saturated tile would compete visually. */
.m3-icon-tile--tonal {
  background: var(--glow);
  color: var(--text-accent);
}
.m3-icon-tile--tonal svg {
  color: var(--text-accent);
}

/* Flat subtractor — transparent box, accent glyph only.  The lightest
   accent treatment in the tile family; use when the box itself is
   contextually unnecessary (e.g. a dense list with a single visual
   anchor per row already supplied by adjacent chrome). */
.m3-icon-tile--flat {
  background: transparent;
  color: var(--text-accent);
}
.m3-icon-tile--flat svg {
  color: var(--text-accent);
}

/* ── .card--hover-wash — dense-row hover wash ──────────────────────────
   Three-layer linear-gradient that re-paints the canonical bevel with
   a faintly-tinted padding-box fill:
     1. `--surface-tint-1`        (canonical hover tint, theme-aware)
     2. `--surface`               (parent fill, prevents page-bg bleed-through)
     3. white-15 → black-25 bevel (preserves raised-paper edge reading)
   Use for dense list rows where a `translateY` lift would compete
   with neighbouring rows.  Consumers should declare their own
   `transition` so the wash animates smoothly — the canonical
   `.m3-card` transition (`background`, `box-shadow`) already covers
   it; add `transform` if the consumer also wants the row to nudge
   on hover.

   Phase 5f: legacy `.net-rank-row:hover` alias opted into via
   `frontend/src/styles/kit-aliases.css` (app-side roster). */
.card--hover-wash:hover {
  /* Tint + parent fill brighten the row on hover; the glass rim stays
     identical to rest via `--container-bevel` so the bevel treatment
     doesn't flip between resting and hover. */
  background:
    linear-gradient(var(--surface-tint-1), var(--surface-tint-1)) padding-box,
    linear-gradient(var(--surface), var(--surface)) padding-box,
    var(--container-bevel);
}

/* ── .card--hover-lift — single-pop hover lift ───────────────────────
   `translateY(-4px)` + brighter accent-tonal fill
   (`--sys-bluepurple-500-20`) + an outsized drop shadow.  Use for
   single-pop emphasis cards in a grid (feature stat tiles, hero CTAs)
   where the card reads as a discrete affordance rather than a
   continuous list.  Consumers must include `transform` in their
   `transition` for the lift to animate smoothly; `.m3-card`'s base
   transition only covers `background` and `box-shadow`.

   Phase 5f: legacy `.outpost-stat-card:hover` alias opted into via
   `frontend/src/styles/kit-aliases.css` (app-side roster). */
.card--hover-lift:hover {
  background: var(--sys-bluepurple-500-20);
  transform: translateY(-4px);
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- canonical hover-lift drop shadow; could be tokenized as --shadow-hover-lift in a follow-up */
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}

/* Divider */
.m3-divider        { border: none; border-top: 1px solid var(--border-subtle); margin: 0; }
.m3-divider--inset  { margin-inline-start: var(--sp-4); }

/* ListTile */
.m3-list-tile {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 56px;
  padding: var(--sp-2) var(--sp-4);
  transition: background var(--dur-short-2) var(--ease-standard);
}
.m3-list-tile--clickable       { cursor: pointer; }
.m3-list-tile--clickable:hover { background: var(--surface-hover); }
.m3-list-tile__leading {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: var(--icon-md);
  color: var(--text-muted);
}
.m3-list-tile__content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-0_5);
}
.m3-list-tile__title {
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  letter-spacing: var(--ls-body-lg);
}
.m3-list-tile__subtitle {
  font-size: var(--fs-body-md);
  line-height: var(--lh-body-md);
  letter-spacing: var(--ls-body-md);
  color: var(--text-muted);
}
.m3-list-tile__trailing {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  color: var(--text-muted);
  font-size: var(--fs-label-sm);
}

}

/* ── ui-kit.components: src/styles/components/navigation.css ── */
@layer ui-kit.components {
/* ── Navigation: .m3-nav-bar + .m3-nav-rail + .m3-nav-drawer + .m3-tab-bar + .m3-app-bar ─ */

/* ═══ Navigation ═══════════════════════════════════════════════════════ */

/* AppBar */
.m3-app-bar {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  z-index: var(--z-nav);
}
.m3-app-bar__row {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 64px;
  padding-inline: var(--sp-1);
}
.m3-app-bar__leading {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.m3-app-bar__title {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-title-lg);
  line-height: var(--lh-title-lg);
  letter-spacing: var(--ls-title-lg);
  font-weight: var(--fw-medium);
}
.m3-app-bar--center-aligned .m3-app-bar__title { text-align: center; }
.m3-app-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  flex-shrink: 0;
}
.m3-app-bar__headline {
  padding: 0 var(--sp-4) var(--sp-7);
}
.m3-app-bar--medium .m3-app-bar__headline {
  font-size: var(--fs-headline-sm);
  line-height: var(--lh-headline-sm);
  letter-spacing: var(--ls-headline-sm);
  font-weight: var(--fw-semibold);
}
.m3-app-bar--large .m3-app-bar__headline {
  font-size: var(--fs-headline-md);
  line-height: var(--lh-headline-md);
  letter-spacing: var(--ls-headline-md);
  font-weight: var(--fw-semibold);
}

/* NavigationBar (bottom)
   The bar itself paints with the canonical card fill (`--surface`) —
   one neutral step above the page bg — so the entire nav surface
   reads as a card-tier chrome strip rather than the elevated-card
   raised step it used to sit on (`--bg-raised`).  Pairs with the
   filled-button recipe on the active item below so the saturated
   pill reads against the quiet card-fill body. */
.m3-nav-bar {
  display: flex;
  align-items: stretch;
  height: 80px;
  background: var(--surface);
  border-top: 1px solid var(--border-subtle);
  z-index: var(--z-nav);
}
.m3-nav-bar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-label-md);
  line-height: var(--lh-label-md);
  letter-spacing: var(--ls-label-md);
  cursor: pointer;
  transition: color var(--dur-short-3) var(--ease-standard);
  padding: var(--sp-3) 0 var(--sp-4);
}
/* Hover lifts the label to the dominant accent tone (matches the
   `--text-accent` step / connect-link / global :hover label color).
   The pill below picks up the saturated bluepurple-300 fill so the
   pair reads as a "primed" affordance. */
/* `--text-accent` = bluepurple-200 (dark/dim) / bluepurple-600 (light): WCAG AA for label-md nav text. */
.m3-nav-bar__item:hover { color: var(--text-accent); }
/* Active item label — bluepurple-300 (`--accent`), theme-stable.  The
   icon glyph inside the pill is forced to zinc-100 below so it reads
   as the filled-button family's bright glyph on the saturated pill,
   while the label outside the pill stays accent-tinted. */
.m3-nav-bar__item--active { color: var(--text-accent); }
.m3-nav-bar__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 32px;
  border-radius: var(--ctl-shape-rect);
  transition: background var(--dur-short-3) var(--ease-standard),
              color var(--dur-short-3) var(--ease-standard);
}
/* Pill recipe — canonical `.m3-btn--filled`: literal `#595fb4`
   (theme-stable bluepurple-300, same hex as `--state-filled-bg`) +
   `--sys-zinc-100` glyph.  Rest = hover per the design retune, just
   like `.m3-tab--active::before` — the dominant -300 highlight tone
   collapses both states onto the SAME saturated step.  Hover on any
   inactive nav-bar item ALSO paints the pill with `--state-hover-bg`
   (also `#595fb4`) so previewing a tab gives the same saturated lift
   as committing to it; the only delta is the label color (muted at
   rest, accent on hover and active). */
.m3-nav-bar__item:hover .m3-nav-bar__icon {
  background: var(--state-hover-bg);
  color: var(--sys-zinc-100);
}
.m3-nav-bar__item--active .m3-nav-bar__icon {
  background: var(--state-filled-bg);
  color: var(--sys-zinc-100);
}
.m3-nav-bar__label { font-weight: var(--fw-medium); }

/* NavigationRail (side) */
.m3-nav-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 80px;
  padding: var(--sp-2) 0;
  background: var(--bg);
  border-right: 1px solid var(--border);
  flex-shrink: 0;
}
.m3-nav-rail--start .m3-nav-rail__items { justify-content: flex-start; }
.m3-nav-rail--center .m3-nav-rail__items { justify-content: center; }
.m3-nav-rail--end .m3-nav-rail__items { justify-content: flex-end; }
.m3-nav-rail__fab {
  padding: var(--sp-2) 0 var(--sp-4);
}
.m3-nav-rail__items {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  flex: 1;
}
.m3-nav-rail__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-label-md);
  line-height: var(--lh-label-md);
  cursor: pointer;
  padding: 0;
  transition: color var(--dur-short-3) var(--ease-standard);
}
.m3-nav-rail__item:hover { color: var(--text); }
.m3-nav-rail__item--active { color: var(--text-accent); }
.m3-nav-rail__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 32px;
  border-radius: var(--ctl-shape-rect);
  transition: background var(--dur-short-3) var(--ease-standard);
}
.m3-nav-rail__item--active .m3-nav-rail__icon { background: var(--accent-subtle); }
.m3-nav-rail__label { font-weight: var(--fw-medium); }

/* NavigationDrawer
   Motion-driven enter/exit — see
   `src/components/material/NavigationDrawer.tsx` and `transitions.
   shelfEnter` / `transitions.shelfExit` / `variants.drawerLeft` /
   `variants.scrim` in `src/lib/motion/config.ts`.  CSS carries layout +
   `pointer-events` only.

   DOM structure (3 layers, modal mode):
     .m3-nav-drawer-scrim  — fixed scrim, Motion-animates opacity
     .m3-nav-drawer-anim   — leading-edge animation wrapper,
                             Motion-animates translateX (slide in)
     .m3-nav-drawer        — static glass surface inside the wrapper */
.m3-nav-drawer-scrim {
  position: fixed;
  /* inset: 0 — expanded for Safari <14.1 / older Firefox */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: var(--z-modal);
  background: var(--overlay);
  /* Scrim carries the page-wide backdrop blur so the cartography /
     page content beneath reads as frosted + dimmed while the drawer
     is open.  Pairs with the inner `.m3-nav-drawer`'s own
     `backdrop-filter` (from the `.m3-modal-glass` chain) for the
     standard "frosted pane over softly-frosted backdrop" hierarchy. */
  backdrop-filter: var(--blur-sm);
  pointer-events: none;
  /* Prevent background scroll bleed on touch devices */
  overscroll-behavior: contain;
  -webkit-tap-highlight-color: transparent;
}
.m3-nav-drawer-scrim--open {
  pointer-events: auto;
}
/* Animation wrapper — leading-edge column that Motion slides in via
   `translateX(-100%) → 0`.  Width matches the inner drawer so the
   `-100%` translation moves it fully off-screen.  No glass treatment;
   the inner `.m3-nav-drawer` carries the backdrop blur.
   Override `--nav-drawer-width` on the host to resize without forking. */
.m3-nav-drawer-anim {
  position: relative;
  width: var(--nav-drawer-width, 360px);
  max-width: 85vw;
  height: 100%;
}
/* Hamburger drawer — layout-only here.  Chrome (page-bg glass +
   canonical card shadow stack, no outline) is painted by the chained
   `.m3-modal-glass` rule above. */
.m3-nav-drawer {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  border-radius: 0 var(--shape-lg) var(--shape-lg) 0;
  padding: var(--sp-3) var(--sp-3);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;     /* smooth momentum scroll — older iOS */
  overscroll-behavior: contain;          /* prevent scroll chaining to body */
  scrollbar-width: none;                 /* Firefox: hide scrollbar */
  -ms-overflow-style: none;              /* IE / legacy Edge: hide scrollbar */
}
.m3-nav-drawer::-webkit-scrollbar { display: none; }  /* Chrome / Safari / Edge: hide scrollbar */
.m3-nav-drawer__headline {
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
  font-size: var(--fs-title-sm);
  line-height: var(--lh-title-sm);
  letter-spacing: var(--ls-title-sm);
  color: var(--text-muted);
  font-weight: var(--fw-medium);
}
.m3-nav-drawer__item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 56px;
  padding: 0 var(--sp-6) 0 var(--sp-4);
  border: none;
  border-radius: var(--ctl-shape-rect);
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-label-lg);
  line-height: var(--lh-label-lg);
  letter-spacing: var(--ls-label-lg);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition: background var(--dur-short-2) var(--ease-standard),
              color var(--dur-short-2) var(--ease-standard);
}
.m3-nav-drawer__item:hover { background: var(--sys-bluepurple-500-12); color: var(--text); }
.m3-nav-drawer__item--active {
  background: var(--accent-subtle);
  color: var(--text-accent);
}
.m3-nav-drawer__icon { flex-shrink: 0; font-size: var(--icon-md); }
.m3-nav-drawer__label { flex: 1; text-align: start; }
.m3-nav-drawer__badge { flex-shrink: 0; }
.m3-nav-drawer__items { flex: 1; }
.m3-nav-drawer__footer {
  margin-top: auto;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border-subtle);
}

/* TabBar */
.m3-tab-bar {
  display: flex;
  border-bottom: 1px solid var(--border-subtle);
  overflow-x: auto;
  scrollbar-width: none;
}
.m3-tab-bar::-webkit-scrollbar { display: none; }
.m3-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-0_5);
  min-height: 48px;
  /* Symmetric `--sp-4` padding on both axes — the inline value matches
     the `.m3-list-tile` padding-inline used by every Status panel row
     (Region / Model / Firmware / Client icon tiles), so the tab nav
     and the rows below it share one canonical horizontal inset.  The
     block value bumps up to match so the tab "button" reads as a
     square-ish tap target rather than a wide-shallow strip. */
  padding: var(--sp-4);
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-title-sm);
  line-height: var(--lh-title-sm);
  letter-spacing: var(--ls-title-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  position: relative;
  /* Layer for the M3 "state layer" hover/press wash painted by
     `.m3-tab::before` below.  Tabs are clickable controls, so we
     reuse the canonical state-layer pattern (transparent at rest,
     accent-tinted on hover, slightly stronger on press) instead of
     the previous color-only transition that read as static. */
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-short-3) var(--ease-standard);
}
/* State-layer pseudo — inset rounded pill that paints hover / active
   state.  Sits inside the tab's tap target with a symmetric `--sp-2`
   margin on all four sides so the badge breathes evenly with the
   parent tab's `--sp-4` padding (8 px pill inset inside a 16 px tab
   padding = visually balanced "frame" around the saturated fill).
   Rounds at `--shape-md` for a chip-like silhouette — same recipe
   family as the existing `m3-chip--filled` / `pkt-badge--region`
   interactive-badge classes, re-tooled for the tab nav.  z-index: 0
   keeps the pill below the children (which lift to z-index: 1 below)
   so the saturated fill never covers the label or icon at any state. */
.m3-tab::before {
  content: "";
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  bottom: var(--sp-2);
  left: var(--sp-2);
  z-index: 0;
  background: transparent;
  border-radius: var(--shape-md);
  pointer-events: none;
  transition: background var(--dur-short-2) var(--ease-standard);
}
.m3-tab > * { position: relative; z-index: 1; }  /* keep label/icon above ::before */
.m3-tab:hover { color: var(--text); }
.m3-tab:hover::before { background: var(--surface-tint-2); }
.m3-tab:active::before { background: var(--surface-tint-3); }
.m3-tab:focus-visible { outline: none; }
.m3-tab:focus-visible::before {
  background: var(--surface-tint-2);
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}
/* Active tab — canonical `.m3-btn--filled` recipe: literal `#595fb4`
   (theme-stable bluepurple-300, same hex as `--state-filled-bg`) +
   `--sys-zinc-100` glyph, theme-stable.  Rest = hover per the design
   retune — the dominant -300 highlight tone collapses both states
   onto the SAME saturated step, mirroring the way the global :hover
   rule paints `--state-hover-bg` (also `#595fb4`) on every fully-
   rounded M3 control.  Previously the rest sat at bluepurple-500
   (`#3e3e96`, deeper) + `--on-primary` (per-mode pivot — deep -950
   in dark / dim, zinc-100 in light); the saturated baseline now
   matches the filled-button family so every accent-active control
   across the system paints the SAME bluepurple-300 fill regardless
   of theme.  The legacy 3 px bottom underline is suppressed (the
   pill is the active indicator now); per-variant underline-height
   rules below become moot but stay in place for any future return
   to the underline cue. */
.m3-tab--active { color: var(--sys-zinc-100); }
.m3-tab--active::before { background: var(--state-filled-bg); }
/* Hover holds the SAME saturated -300 hex as rest — the filled-button
   family no longer carries a per-variant brightness step between rest
   and hover, so the active tab follows suit.  Both states paint
   `--state-filled-bg` / `--state-hover-bg` (both `#595fb4`) + zinc-100
   for the glyph. */
.m3-tab--active:hover { color: var(--sys-zinc-100); }
.m3-tab--active:hover::before { background: var(--state-hover-bg); }
.m3-tab--active::after { content: none; }
.m3-tab-bar--primary .m3-tab--active::after { height: 3px; }
.m3-tab-bar--secondary .m3-tab--active::after { height: 2px; }
.m3-tab__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--icon-md);
  /* Subtle scale-up of the icon when its tab is active — reinforces the
     selection alongside the underline + accent color, so the active tab
     reads as the primary action rather than a static label. */
  transition: transform var(--dur-short-3) var(--ease-emphasized-decel);
}
.m3-tab--active .m3-tab__icon { transform: scale(1.08); }
.m3-tab__label { white-space: nowrap; }


}

/* ── ui-kit.components: src/styles/components/selection.css ── */
@layer ui-kit.components {
/* ── Selection: .m3-checkbox + .m3-chip + .m3-select + .m3-radio + .m3-slider + .m3-switch ─ */

/* ═══ Selection ════════════════════════════════════════════════════════ */

/* Checkbox */
.m3-checkbox {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 40px;
  height: 40px;
  cursor: pointer;
}
.m3-checkbox--disabled { opacity: 0.38; cursor: default; }
.m3-checkbox__input {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: inherit;
  z-index: 1;
}
.m3-checkbox__box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 2px solid var(--text-muted);
  border-radius: var(--shape-2xs);
  transition: background var(--dur-short-2) var(--ease-standard),
              border-color var(--dur-short-2) var(--ease-standard);
}
.m3-checkbox__input:checked ~ .m3-checkbox__box,
.m3-checkbox__input:indeterminate ~ .m3-checkbox__box {
  background: var(--accent);
  border-color: var(--accent);
}
.m3-checkbox__check {
  width: 18px;
  height: 18px;
  color: var(--on-primary);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity var(--dur-short-2) var(--ease-standard),
              transform var(--dur-short-2) var(--ease-standard);
}
.m3-checkbox__input:checked ~ .m3-checkbox__box .m3-checkbox__check,
.m3-checkbox__input:indeterminate ~ .m3-checkbox__box .m3-checkbox__check {
  opacity: 1;
  transform: scale(1);
}
.m3-checkbox__input:focus-visible ~ .m3-checkbox__box {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* Chip — 32-dp control, recipe shared with .m3-select__trigger via the
   --ctl-* tokens.  The visible neutral fill gives the control its body;
   the hairline gradient bevel rim is painted by the shared control
   contract in `contracts.css` (`::after` mask ring, corner-pinned
   `--container-bevel`) so every rounded control shares one rim-light
   direction with the cards.  Hover steps one notch up the neutral
   ladder; selection paints the accent-tonal fill so the active state
   stands out. */
.m3-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--ctl-h-compact);
  padding: 0 var(--ctl-pad-inline);
  border: none;
  border-radius: var(--ctl-shape-rect);
  background: var(--ctl-neutral-bg);
  color: var(--text);
  font: inherit;
  font-size: var(--ctl-fs);
  line-height: var(--lh-label-lg);
  font-weight: var(--ctl-fw);
  letter-spacing: var(--ctl-ls);
  cursor: pointer;
  transition: background var(--dur-short-2) var(--ease-standard),
              color var(--dur-short-2) var(--ease-standard);
}
.m3-chip:hover { background: var(--ctl-neutral-bg-hover); }
/* Selected chip — same tonal recipe as `.m3-btn--tonal`: -300 alpha bg
   + `--sys-bluepurple-300` glyph (bright -300 in dark / dim, palette-
   pivoted to deep -800 in light). */
.m3-chip--selected {
  background: var(--state-tonal-bg);
  color: var(--sys-zinc-100);
}
/* Filled chip — saturated accent fill + on-primary glyph.  Mirrors the
   `.m3-btn--filled` recipe so consumers can opt a chip into the same
   high-emphasis treatment buttons get (used for region/IATA-style
   identifier badges that should read as a saturated tag rather than
   the lower-contrast tonal selection state). */
.m3-chip--filled {
  background: var(--state-filled-bg);
  color: var(--sys-zinc-100);
}
.m3-chip--filled:hover { background: var(--state-filled-bg-hover); }
.m3-chip--filter .m3-chip__icon { font-size: var(--icon-sm); }
.m3-chip__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--icon-sm);
  /* Pin every icon slot to the same intrinsic box so SVG/lucide icons sit
     identically on iOS Safari, Android Chrome, desktop Firefox/Chrome.
     Prevents drift from the chip's flex baseline calculation when an icon
     has a different intrinsic line-height than the label text next to it. */
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  line-height: 1;
}
.m3-chip__icon > svg { display: block; }  /* kills inline-baseline gap */
.m3-chip__label { white-space: nowrap; }

/* Icon-only chip — square, centered, no asymmetric label padding/gap.
   Used by the LivePackets "improbable links" filter (and any other
   filter chip that wants a pure-icon trigger).  Rendered as a 32×32
   square so the hit target meets WCAG / iOS / Android touch-target
   minimums (Apple HIG: 44pt is preferred but 32px is the documented
   M3 chip size; the chip's :focus-visible outline still extends to the
   M3 spec). */
.m3-chip--icon-only {
  width: 32px;
  padding: 0;
  gap: 0;
  justify-content: center;
}
/* Body button in the dual-interactive chip pattern (rendered when `onDelete`
   is present).  The outer `.m3-chip` provides the container chrome; this
   inner `<button>` acts as the main chip action and must reset native button
   chrome while inheriting the chip's label/icon layout. */
.m3-chip__body {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: 1;
  min-width: 0;
}
.m3-chip__body:disabled { cursor: default; }
.m3-chip__delete {
  display: flex;
  align-items: center;
  margin-inline-start: calc(var(--sp-1) * -1);
  color: var(--text-muted);
  cursor: pointer;
  border-radius: var(--shape-full);
  transition: color var(--dur-short-2) var(--ease-standard);
}
.m3-chip__delete:hover { color: var(--text); }
.m3-chip:disabled {
  opacity: 0.38;
  cursor: default;
}

/* Select (exposed dropdown menu) */
.m3-select {
  position: relative;
  display: inline-flex;
}
.m3-select__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--ctl-h-compact);
  padding: 0 var(--ctl-pad-inline);
  border: none;
  border-radius: var(--ctl-shape-rect);
  /* Visible neutral fill at rest so the trigger reads as a real
     control; the shared control contract (`contracts.css`) adds the
     hairline gradient bevel rim on top.  Hover / open state bumps one
     notch up the context-aware neutral ladder. */
  background: var(--ctl-neutral-bg);
  color: var(--text);
  font: inherit;
  font-size: var(--ctl-fs);
  line-height: var(--ctl-lh);
  font-weight: var(--ctl-fw);
  letter-spacing: var(--ctl-ls);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-short-2) var(--ease-standard);
}
.m3-select__trigger:hover            { background: var(--ctl-neutral-bg-hover); }
.m3-select--open .m3-select__trigger { background: var(--ctl-neutral-bg-hover); }
.m3-select__label { flex: 1; text-align: start; }
.m3-select__label--placeholder { color: var(--text-muted); }
.m3-select__icon {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--dur-short-2) var(--ease-standard);
}
.m3-select--open .m3-select__icon { transform: rotate(180deg); }
.m3-select--bottom .m3-select__icon { transform: rotate(180deg); }
.m3-select--bottom.m3-select--open .m3-select__icon { transform: rotate(0deg); }
.m3-radio {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 40px;
  height: 40px;
  cursor: pointer;
}
.m3-radio--disabled { opacity: 0.38; cursor: default; }
.m3-radio__input {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: inherit;
  z-index: 1;
}
.m3-radio__circle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 2px solid var(--text-muted);
  border-radius: var(--shape-full);
  transition: border-color var(--dur-short-2) var(--ease-standard);
}
.m3-radio__input:checked ~ .m3-radio__circle { border-color: var(--accent); }
.m3-radio__dot {
  width: 10px;
  height: 10px;
  border-radius: var(--shape-full);
  background: var(--accent);
  transform: scale(0);
  transition: transform var(--dur-short-2) var(--ease-standard);
}
.m3-radio__input:checked ~ .m3-radio__circle .m3-radio__dot { transform: scale(1); }
.m3-radio__input:focus-visible ~ .m3-radio__circle {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* Slider */
.m3-slider {
  display: inline-flex;
  align-items: center;
  position: relative;
  width: 100%;
  height: 40px;
  cursor: pointer;
}
.m3-slider--disabled { opacity: 0.38; cursor: default; }
.m3-slider__input {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: inherit;
  z-index: 1;
}
.m3-slider__track {
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: var(--shape-2xs);
  background: var(--surface-active);
}
.m3-slider__fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: var(--m3-slider-pct, 0%);
  border-radius: var(--shape-2xs);
  background: var(--accent);
  transition: width var(--dur-short-2) var(--ease-standard);
}
.m3-slider__thumb {
  position: absolute;
  top: 50%;
  left: var(--m3-slider-pct, 0%);
  width: 20px;
  height: 20px;
  border-radius: var(--shape-full);
  background: var(--accent);
  transform: translate(-50%, -50%);
  box-shadow: var(--elevation-1);
  transition: left var(--dur-short-2) var(--ease-standard),
              box-shadow var(--dur-short-2) var(--ease-standard);
}
.m3-slider__input:focus-visible ~ .m3-slider__track .m3-slider__thumb {
  box-shadow: 0 0 0 8px var(--accent-subtle);
}

/* Switch */
.m3-switch {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  position: relative;
}
.m3-switch--disabled { opacity: 0.38; cursor: default; }
.m3-switch__input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.m3-switch__track {
  display: flex;
  align-items: center;
  width: 52px;
  height: 32px;
  border-radius: var(--shape-full);
  background: var(--surface-active);
  border: 2px solid var(--text-muted);
  padding: 0 var(--sp-0_5);
  transition: background var(--dur-short-3) var(--ease-standard),
              border-color var(--dur-short-3) var(--ease-standard);
}
.m3-switch--checked .m3-switch__track {
  background: var(--accent);
  border-color: var(--accent);
}
.m3-switch__thumb {
  width: 16px;
  height: 16px;
  border-radius: var(--shape-full);
  background: var(--text-muted);
  transition: transform var(--dur-short-3) var(--ease-standard),
              width var(--dur-short-3) var(--ease-standard),
              height var(--dur-short-3) var(--ease-standard),
              background var(--dur-short-3) var(--ease-standard);
}
.m3-switch--checked .m3-switch__thumb {
  transform: translateX(20px);
  width: 24px;
  height: 24px;
  background: var(--on-primary);
}
.m3-switch__input:focus-visible ~ .m3-switch__track {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* Menu — popover / dropdown.  Chrome (page-bg glass + canonical card
   shadow stack, no outline) is painted by the chained `.m3-modal-glass`
   rule above.  Motion-driven enter/exit — see
   `src/components/material/Menu.tsx` and `transitions.menuEnter` /
   `transitions.menuExit` in `src/lib/motion/config.ts`.

   DOM structure (2 layers):
     .m3-menu-anim  — fixed-positioned animation wrapper (top/left from
                       useViewportClamp), Motion-animates scaleY + opacity
     .m3-menu       — static glass surface inside the wrapper */
.m3-menu-anim {
  position: fixed;
  z-index: var(--z-overlay);
  /* No glass; transform-origin is set inline based on placement. */
}
.m3-menu {
  min-width: 112px;
  max-width: 280px;
  border-radius: var(--shape-sm);
  padding: var(--sp-2) 0;
}
.m3-menu__item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  min-height: 36px;
  padding: 0 var(--sp-3);
  border: none;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  cursor: pointer;
  transition: background var(--dur-short-2) var(--ease-standard);
}
/* Visible hover on menu items — `--surface-hover` is transparent in
   dark/dim, so reach for the context-aware neutral fill instead so the
   hover affordance is legible regardless of theme. */
.m3-menu__item:hover { background: var(--ctl-neutral-bg); }
.m3-menu__item:disabled { opacity: 0.38; cursor: default; }
.m3-menu__icon { flex-shrink: 0; font-size: var(--icon-md); color: var(--text-muted); }
.m3-menu__label { flex: 1; text-align: start; }
.m3-menu__divider { border: none; border-top: 1px solid var(--border-subtle); margin: var(--sp-2) 0; }


}

/* ── ui-kit.components: src/styles/components/text-input.css ── */
@layer ui-kit.components {
/* ── Text Input: .m3-text-field family ──────────────────────────────────── */

/* ═══ Text Input ══════════════════════════════════════════════════════ */

/* TextField */
.m3-text-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  position: relative;
}
.m3-text-field--disabled { opacity: 0.38; }
.m3-text-field__container {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 56px;
  position: relative;
}
/* Compact variant — matches chip / select at 32dp.
   Use on toolbars, table headers, or wherever space is tight. */
.m3-text-field--compact {
  gap: 0;
}
.m3-text-field--compact .m3-text-field__container {
  height: 32px;
  min-height: 32px;
  box-sizing: border-box;
}
.m3-text-field--compact .m3-text-field__inner {
  padding: 0;
}
.m3-text-field--compact .m3-text-field__label {
  display: none; /* compact fields rely on placeholder, not floating label */
}
.m3-text-field--compact .m3-text-field__input {
  font-size: var(--fs-label-lg);
  line-height: var(--lh-label-lg);
}
.m3-text-field--compact .m3-text-field__input::placeholder {
  color: var(--text-muted);
}
/* Outlined variant */
.m3-text-field--outlined .m3-text-field__container {
  background: var(--bg);
  /* Bluepurple-400 (#4a559a = rgb(74, 85, 154)) at 80% — the canonical
     accent-light tone, kept here as a literal rgba so the border still
     reads at low chroma without color-mix() support. */
  /* stylelint-disable-next-line declaration-property-value-disallowed-list -- canonical bluepurple-400 stroke at 80%; tokenize as --state-outlined-stroke-soft if reused */
  border: 1px solid rgba(74, 85, 154, 0.8);
  border-radius: var(--shape-sm);
  padding: 0 var(--sp-4);
  transition: border-color var(--dur-short-2) var(--ease-standard);
}
.m3-text-field--outlined.m3-text-field--focused .m3-text-field__container {
  border-color: var(--accent);
  border-width: 2px;
  padding: 0 calc(var(--sp-4) - 1px);
}
.m3-text-field--outlined.m3-text-field--error .m3-text-field__container {
  border-color: var(--sys-red-500);
}
/* Filled variant */
.m3-text-field--filled .m3-text-field__container {
  background: var(--surface);
  border-radius: var(--shape-sm);
  border-bottom: 1px solid var(--text-muted);
  padding: 0 var(--sp-4);
  transition: border-color var(--dur-short-2) var(--ease-standard);
}
.m3-text-field--filled.m3-text-field--focused .m3-text-field__container {
  border-bottom: 2px solid var(--accent);
  padding-bottom: 0;
}
.m3-text-field--filled.m3-text-field--error .m3-text-field__container {
  border-bottom-color: var(--sys-red-500);
}
/* Inner (label + input stack) */
.m3-text-field__inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  padding: var(--sp-2) 0;
}
/* Floating label */
.m3-text-field__label {
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
  pointer-events: none;
  transform-origin: top left;
  transition: transform var(--dur-short-3) var(--ease-standard),
              font-size var(--dur-short-3) var(--ease-standard),
              color var(--dur-short-3) var(--ease-standard);
}
.m3-text-field--label-up .m3-text-field__label {
  font-size: var(--fs-body-sm);
  /* Floating label lifts into small text — use passing step. */
  color: var(--text-accent);
}
.m3-text-field--error .m3-text-field__label { color: var(--sys-red-500); }
/* Input */
.m3-text-field__input {
  width: 100%;
  border: none;
  outline: none;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body-lg);
  padding: 0;
}
.m3-text-field__input::placeholder { color: transparent; }
.m3-text-field--label-up .m3-text-field__input::placeholder,
.m3-text-field--focused .m3-text-field__input::placeholder {
  color: var(--text-tertiary);
}
/* Icons */
.m3-text-field__icon {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  font-size: var(--icon-md);
  color: var(--text-muted);
}
/* Helper text */
.m3-text-field__helper {
  padding: 0 var(--sp-4);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-muted);
}
.m3-text-field--error .m3-text-field__helper { color: var(--sys-red-500); }

/* Mono identifier modifier — paints the input glyph in `var(--mono)` and
   uppercases its content.  Canonical recipe for tokenised fixed-shape
   identifiers (IATA codes, pubkey prefixes, hex flags, callsigns).
   Scoped to the input + its placeholder so the floating label retains its
   canonical sans-serif treatment (label is metadata, not content). */
.m3-text-field--mono .m3-text-field__input,
.m3-text-field--mono .m3-text-field__input::placeholder {
  font-family: var(--mono);
  text-transform: uppercase;
}


}

/* ── ui-kit.components: src/styles/components/text.css ── */
@layer ui-kit.components {
/* ── .m3-text — canonical typography primitive ───────────────────────
   Single class family backing the kit's `<Text>` component.  Replaces
   the previous consumer pattern of hand-rolling typography +
   tone via `<span style={{ fontSize: var(--fs-*), lineHeight: var(--lh-*),
   color: var(--text-*) }}>` chains.  Phase R Survey across the
   consumer surface (frontend + admin .tsx) found 417 inline reads of
   the same six tokens (--text-muted, --mono, --fs-label-xs,
   --text-tertiary, --fs-label-sm, --fw-medium); this primitive
   absorbs the pattern.

   Six orthogonal axes:
     • tone     — default | muted | tertiary | accent
     • size     — label-xs | label-sm | label-md | label-lg
                | body-sm  | body-md  | body-lg
                | title-sm | title-md
                | headline-sm | headline-md
     • weight   — regular | medium | semibold
     • family   — sans (default) | mono
     • align    — left (default) | right | center
     • uppercase (boolean modifier — pairs with --ls-eyebrow when set)

   Every axis is independent; the React component composes the
   class set via the `cx()` helper.  Base rule below establishes the
   inherit defaults so an unmodified `<Text>` renders as the parent's
   body type — only the modifiers override. */

.m3-text {
  /* All defaults inherit so an unstyled `<Text>` is invisible (paints
     as plain inherited body type).  Override via modifier classes. */
  color: inherit;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  font-weight: inherit;
  text-align: inherit;
  text-transform: none;
  letter-spacing: inherit;
}

/* ── Tone (color) ────────────────────────────────────────────────── */
.m3-text--tone-muted     { color: var(--text-muted); }
.m3-text--tone-tertiary  { color: var(--text-tertiary); }
.m3-text--tone-accent    { color: var(--text-accent); }

/* ── Size (font-size + paired line-height) ───────────────────────── */
.m3-text--size-label-xs   { font-size: var(--fs-label-xs);   line-height: var(--lh-label-xs); }
.m3-text--size-label-sm   { font-size: var(--fs-label-sm);   line-height: var(--lh-label-sm); }
.m3-text--size-label-md   { font-size: var(--fs-label-md);   line-height: var(--lh-label-md); }
.m3-text--size-label-lg   { font-size: var(--fs-label-lg);   line-height: var(--lh-label-lg); }
.m3-text--size-body-sm    { font-size: var(--fs-body-sm);    line-height: var(--lh-body-sm); }
.m3-text--size-body-md    { font-size: var(--fs-body-md);    line-height: var(--lh-body-md); }
.m3-text--size-body-lg    { font-size: var(--fs-body-lg);    line-height: var(--lh-body-lg); }
.m3-text--size-title-sm   { font-size: var(--fs-title-sm);   line-height: var(--lh-title-sm); }
.m3-text--size-title-md   { font-size: var(--fs-title-md);   line-height: var(--lh-title-md); }
.m3-text--size-title-lg   { font-size: var(--fs-title-lg);   line-height: var(--lh-title-lg); }

/* Monospace period micro-kern — JetBrains Mono seats every glyph in a full
   cell, so a "." reads aggressively spaced at display sizes.  Isolating the
   dot (see tightenMonoDots) and pulling both neighbours in trims that gap
   without disturbing the mono grid anywhere else.  Symmetric ("either side"),
   tuned to sit just shy of touching the adjacent glyphs. */
.m3-mono-dot { margin-inline: -0.09em; }
.m3-text--size-headline-sm{ font-size: var(--fs-headline-sm);line-height: var(--lh-headline-sm); }
.m3-text--size-headline-md{ font-size: var(--fs-headline-md);line-height: var(--lh-headline-md); }

/* ── Weight ──────────────────────────────────────────────────────── */
.m3-text--weight-regular  { font-weight: 400; }
.m3-text--weight-medium   { font-weight: var(--fw-medium); }
.m3-text--weight-semibold { font-weight: var(--fw-semibold); }

/* ── Family ──────────────────────────────────────────────────────── */
.m3-text--family-mono     { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ── Align ───────────────────────────────────────────────────────── */
.m3-text--align-left   { text-align: left; }
.m3-text--align-right  { text-align: right; }
.m3-text--align-center { text-align: center; }

/* ── Uppercase eyebrow recipe ────────────────────────────────────────
   Composes the canonical eyebrow tracking (`--ls-eyebrow` = 0.05em
   positive letter-spacing) alongside `text-transform: uppercase`.
   Pair with `size="label-xs"` + `tone="tertiary"` for the canonical
   chart-card eyebrow caption shape. */
.m3-text--uppercase {
  text-transform: uppercase;
  letter-spacing: var(--ls-eyebrow);
}

}

/* ── ui-kit.components: src/styles/components/contracts.css ── */
@layer ui-kit.components {
/* ── Global contracts: gradient bevel ::after + global hover lift + saturated brand fill → on-primary glyph ─ */

/* ── Gradient bevel outline for rounded controls ────────────────────
   Shares the EXACT bevel the card system paints via `--container-bg`
   (the corner-pinned `--container-bevel` — a `to bottom right` sweep
   `#ABABAB` → `#454545` → faded `#ABABAB`) and applies it as a 0.5 px
   outline to every fully-rounded control: M3 buttons, icon buttons,
   FABs, chips, segmented containers, select triggers, and the status
   pill.  Reusing the same token means chips / filter buttons / cards
   all catch light from the SAME direction in every theme — one unified
   rim-light across the app.  Deliberately NO inner shadow here: these
   are flush controls, not lifted panels, so only the rim is shared.

   Implemented as an inset `::after` pseudo-element plus mask-composite
   so the gradient paints ONLY in a 0.5 px frame over each control's
   existing fill — the fill (and its animatable hover transition) is
   left untouched, and we don't have to thread the bevel through every
   variant's `background` declaration.

   The bevel is corner-pinned (`to bottom right`, not a fixed angle) so
   a wide segmented group or filter pill keeps the bright stop anchored
   to its top-left corner instead of stretching — same behaviour as the
   card rims.

   `border-radius: inherit` keeps the outline shape locked to the
   underlying control corner; `pointer-events: none` keeps the
   pseudo-overlay from intercepting hover / click events. */
/* Phase 5f: kit owns the M3-prefixed roster only.  Legacy app-class
   icon-tile aliases (`.nd-status-icon-tile`, `.connect-section-icon`,
   `.edge-shelf-disclosure__icon`) opt into the bevel via the app-side
   roster (kit-aliases.css). */
.m3-btn,
.m3-icon-btn,
.m3-chip,
.m3-select__trigger,
.m3-fab,
.m3-segmented,
.m3-icon-tile {
  position: relative;
}
.m3-btn::after,
.m3-icon-btn::after,
.m3-chip::after,
.m3-select__trigger::after,
.m3-fab::after,
.m3-segmented::after,
.m3-icon-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* Ring width shares the card's `--container-stroke` hairline token so
     controls + cards walk their stroke weight together. */
  padding: var(--container-stroke);
  /* stylelint-disable declaration-property-value-disallowed-list, color-no-hex -- shared corner-pinned gradient bevel (`--container-bevel`, same token the card rims use), masked to a hairline ring; mask layers use #000 as a literal opaque sentinel (alpha sources, not chromatic content) */
  background: var(--container-bevel);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  /* stylelint-enable declaration-property-value-disallowed-list, color-no-hex */
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}
/* Suppress the bevel on the transparent-fill icon-tile subtractor —
   `.m3-icon-tile--flat` paints the box transparent so the 1 px
   gradient ring would float as a fake-outlined chip rather than a
   bevel on a saturated surface.  Same pattern as the
   `.m3-icon-btn--standard::after { content: none; }` rule below. */
.m3-icon-tile--flat::after { content: none; }
/* Suppress the bevel on transparent-rest icon buttons — the
   `--standard` variant is intentionally chrome-quiet (nav hamburger,
   theme toggle, toolbar icons), so the always-on outline reads as
   visual noise.  Hover stays transparent and only lifts the GLYPH
   color (see `.m3-icon-btn--standard:hover` above). */
.m3-icon-btn--standard::after { content: none; }
/* Suppress the bevel on canonical M3 outlined variants — the
   recipe paints its own 1 px solid stroke (`--state-outlined-stroke`
   = bluepurple-400 hex) directly via the `border` declaration, so
   the gradient bevel would otherwise stack a second ring on top of
   the explicit stroke and read as a double outline. */
.m3-btn--outlined::after,
.m3-icon-btn--outlined::after { content: none; }
/* Disabled state — toggle the gradient bevel ::after off so the
   disabled chrome reads as a clean flat zinc fill (set above on the
   `:disabled` rule of each variant family).  Pairs with the
   `background: var(--card-fill-zinc)` + `opacity: 0.38` block on
   `.m3-btn:disabled` / `.m3-icon-btn:disabled` etc., giving every
   disabled control ONE quiet treatment regardless of which variant
   was active. */
.m3-btn:disabled::after,
.m3-icon-btn:disabled::after,
.m3-chip:disabled::after,
.m3-select__trigger:disabled::after,
.m3-fab:disabled::after,
.m3-segmented__btn:disabled::after {
  content: none;
}
/* ── Global hover lift —──────────────────────────────────────────────
   Every fully-rounded badge / button / icon-button / chip / dropdown
   trigger / FAB / segmented option / status pill lifts on hover to
   the canonical dominant highlight tone (`--state-hover-bg` =
   bluepurple-300, `#595fb4`).  THEME-STABLE: the literal hex bypasses
   the light-mode `-300 → -800` palette pivot so the same saturated
   step paints in dark, dim, AND light.

   Hover collapses onto the SAME bluepurple-300 hex as the saturated
   rest tones (filled buttons, filled chips, filled icon button,
   tonal variants), so the progression for those variants reads:
     rest (-300, `#595fb4`)  →  hover (-300, `#595fb4`)  — unified,
     no per-variant brightness step.  For chrome-quiet rest variants
     (neutral / outlined / text / elevated, inactive segmented option,
     FAB), the hover lifts FROM the low-chroma rest INTO the dominant
     -300 highlight tone.  Pairs with `--sys-zinc-100` (theme-stable
     bright near-white) for the glyph so the highlighted label reads
     as one consistent bright glyph across dark / dim / light on the
     saturated -300 fill.  See `--state-hover-bg` declaration in
     `:root` for the full retune rationale.

   `:not(.m3-icon-btn--standard)` excludes the transparent-rest icon
   button variant from the lift — `--standard` hover keeps the bg
   transparent and only lifts the GLYPH color (handled by the
   `.m3-icon-btn--standard:hover` rule above).  Without this exclusion,
   the global rule would paint `--state-hover-bg` over `--standard`
   and defeat the icon-only highlight aesthetic. */
.m3-btn:hover,
.m3-icon-btn:not(.m3-icon-btn--standard):hover,
.m3-chip:hover,
.m3-select__trigger:hover,
.m3-fab:hover,
.m3-segmented__btn:hover {
  background: var(--state-hover-bg);
  color: var(--sys-zinc-100);
}
/* The select-open state adopts the SAME max-brightness lift as hover
   so the trigger reads as "primed" while its menu is open.  Two
   selectors:
     • `.m3-select--open .m3-select__trigger` — canonical <Select>,
       which puts the modifier on the .m3-select wrapper.
     • `.m3-select__trigger.m3-select--open` — hand-rolled triggers
       (RegionSelect / radio-channel) that apply the modifier directly
       on the button.  Pairs with `--on-primary` (per-mode pivot)
       so the open-state glyph tracks the same brightness contract
       as hover.

   `--filled` modifier — OPTIONAL "this control is the active filter
   badge" state.  Consumers tack `m3-select__trigger--filled` onto
   the trigger whenever the dropdown carries a non-empty value so
   the saturated bluepurple-300 fill + zinc-100 glyph reads as a
   permanent filter affordance (same recipe `.m3-chip--filled` paints
   for filter chips elsewhere).  Visually identical to the hover /
   open state painted above because `--state-hover-bg` and
   `--state-filled-bg` resolve to the SAME theme-stable bluepurple-300
   hex (see the `:root` declarations + the Bluepurple-300 hover
   override block below for the cross-token equivalence).  Descendant
   icon + placeholder colors track the same bright-zinc contract the
   hover/open painters establish for consistency. */
.m3-select--open .m3-select__trigger,
.m3-select__trigger.m3-select--open,
.m3-select__trigger--filled {
  background: var(--state-hover-bg);
  color: var(--sys-zinc-100);
}
/* Bluepurple-300 hover override (defense-in-depth) — the global
   `:hover` rule above already paints `--state-hover-bg` = bluepurple-
   300 across the entire M3 control roster after the token retune, so
   in principle every hover state already resolves to `#595fb4`.  This
   block stays in source as belt-and-suspenders: it re-paints the same
   hex on the variants the design spec explicitly anchors at -300 so a
   future re-tune of `--state-hover-bg` (e.g. back to a brighter step)
   does NOT silently regress these specific variants away from the
   dominant highlight tone.  Icon-button variants chain `.m3-icon-btn`
   in front so they land at (0,3,0) and tie the global rule's `:not()`
   specificity; source order then resolves in this rule's favor.

   Roster:
     • `.m3-btn--neutral` / `.m3-icon-btn--neutral` — chrome-quiet
       zinc rest lifts to -300.
     • `.m3-btn--outlined` / `.m3-icon-btn--outlined` — transparent
       rest with bluepurple-400 stroke; saturated -300 fill on hover.
     • `.m3-btn--text` / `.m3-btn--elevated` — transparent / raised-
       card zinc rests.
     • `.m3-icon-btn--filled` — saturated-rest -300 holds at hover.
     • `.m3-fab` — saturated FAB lift.
     • `.m3-segmented__btn` — inactive segmented option's hover. */
.m3-btn--neutral:hover,
.m3-icon-btn.m3-icon-btn--neutral:hover,
.m3-btn--outlined:hover,
.m3-icon-btn.m3-icon-btn--outlined:hover,
.m3-btn--text:hover,
.m3-btn--elevated:hover,
.m3-icon-btn.m3-icon-btn--filled:hover,
.m3-fab:hover,
.m3-segmented__btn:hover {
  background: var(--brand-stable-300);
  color: var(--sys-zinc-100);
}

/* ── Danger tone hover roster ─────────────────────────────────────
   Source-ordered AFTER the canonical bluepurple-300 lift block above
   so the same (0,3,0) specificity resolves in favor of the red
   anchor.  Without this, the global hover painter would repaint every
   `tone="danger"` button with `--state-hover-bg = bluepurple-300` on
   hover, defeating the destructive-action signal at the exact moment
   the user is about to commit.

   Roster:
     • `.m3-btn--tone-danger` — all variant families chain through to
        the saturated red-500 fill + zinc-100 glyph on hover.
     • `.m3-icon-btn--tone-danger` (filled / outlined / tonal) —
        same lift.
     • `.m3-icon-btn--tone-danger.m3-icon-btn--standard:hover` —
        chrome-quiet variant keeps the transparent background and
        only deepens the GLYPH to `--state-danger-bg-hover` (red-600)
        so the toolbar trash glyph reads as a hot affordance without
        a lit tile (mirrors the standard accent contract). */
.m3-btn--tone-danger:hover,
.m3-icon-btn.m3-icon-btn--tone-danger:not(.m3-icon-btn--standard):hover {
  background: var(--state-danger-bg-hover);
  color: var(--sys-zinc-100);
}
.m3-icon-btn.m3-icon-btn--tone-danger.m3-icon-btn--standard:hover {
  background: transparent;
  color: var(--state-danger-bg-hover);
}
/* ── Trigger leading-icon rest color ───────────────────────────────────
   The hand-rolled triggers (RegionSelect MapPin, radio-channel Antenna)
   no longer set their own inline color, so they inherit this CSS-owned
   neutral at rest — matching `.m3-select__icon` exactly so leading +
   trailing icons read as one tonal step.  Direct-child selector so we
   only catch a top-level leading icon, not any SVGs that might appear
   inside slot children of a popover. */
.m3-select__trigger > svg {
  color: var(--text-muted);
  transition: color var(--dur-short-2) var(--ease-standard);
}
/* ── Canonical "saturated brand fill → on-primary glyph" contract ─────────
   THE single source of truth for foreground color on every surface
   that paints a saturated bluepurple fill.  Without this, descendant
   explicit-color elements (chevrons, leading icons, placeholder text,
   the tab label) would stay stuck on their per-class rest color (e.g.
   `.m3-select__icon` → `--text-muted`, `.nd-status-icon` → cyan) and
   read as muted-on-saturated — the recurring "impossible to read"
   hover/active bug.  When adding a new component that paints a
   saturated brand fill, ADD ITS SELECTOR PAIR HERE.  Do not re-derive
   the contract per-consumer.

   Split into TWO BLOCKS by background tier:

   Block A — REST tier (saturated -300 / -500 base) → `--on-primary`
     The per-mode pivot stop: bluepurple-950 in dark/dim,
     bluepurple-50 in light (see `--on-primary` declaration in `:root`).
         `.m3-btn--filled`            (--state-filled-bg = -300 hex)
         `.m3-icon-btn--filled`       (same)
         `.m3-chip--filled`           (same)
         `.m3-tab--active`            (--sys-bluepurple-500 rest)
         `.m3-btn--outlined`          (--state-outlined-bg = --accent = -300)
         `.m3-icon-btn--outlined`     (same)

   Block B — HOVER / OPEN tier (max-brightness -200 lift) →
     `--on-primary` (per-mode pivot — -950 in dark / dim, -50 in
     light).  The lifted bg is theme-stable bluepurple-200, so a
     per-mode glyph gives the light-mode hover a BRIGHTER label than
     rest (matching the design spec) while dark / dim keeps the deep
     dark glyph for ~6:1 contrast.  Consumers: every hover state on
     the rounded control roster, plus the select-open trigger and the
     active-tab hover.

   Outlined variants are split out of this block — their glyph
   inherits the bluepurple-400 stroke color (`--state-outlined-fg`)
   from the parent rule, so we don't override it here.

   Specificity: each selector lands at (0,2,1) or higher, beating the
   (0,1,0) of the per-class rest-color rules.  Source-ordered AFTER
   the rest rules so the cascade resolves cleanly without `!important`. */
/* Bluepurple-300 fill consumers (filled buttons, filled icon button,
   filled chip, tonal variants, segmented selected, selected chip) pin
   their glyph to theme-stable bright zinc-100 — same hex across dark
   / dim / light, matching the text contract set on the parent rules. */
.m3-btn--filled .m3-btn__icon,
.m3-btn--filled svg,
.m3-btn--tonal .m3-btn__icon,
.m3-btn--tonal svg,
.m3-icon-btn--filled svg,
.m3-icon-btn--tonal svg,
.m3-chip--filled .m3-chip__icon,
.m3-chip--filled svg,
.m3-chip--selected .m3-chip__icon,
.m3-chip--selected svg,
.m3-segmented__btn--selected svg {
  color: var(--sys-zinc-100);
}
/* Active tab still sits on bluepurple-500 (not -300), so it keeps the
   `--on-primary` per-mode pivot rather than the theme-stable zinc-100
   pinned on the bluepurple-300 roster above. */
.m3-tab--active .m3-tab__icon,
.m3-tab--active .m3-tab__label,
.m3-tab--active svg {
  color: var(--on-primary);
}
/* Outlined glyph contract — descendant SVGs / icons inside the
   outlined variants paint the same theme-stable bluepurple-400 hex
   as the parent label, so the stroke + glyph + label read as one
   tonal step. */
.m3-btn--outlined .m3-btn__icon,
.m3-btn--outlined svg,
.m3-icon-btn--outlined svg {
  color: var(--state-outlined-fg);
}
/* Hover / open glyph contract — descendant icons + placeholders +
   labels inside any hovered or open control pin to theme-stable
   `--sys-zinc-100`, matching the parent label set by the hover rules
   above.  Bright zinc paints the same hex across dark / dim / light so
   the highlighted state always reads as a bright label on the
   bluepurple-200 / bluepurple-300 hover fill. */
.m3-btn:hover .m3-btn__icon,
.m3-btn:hover svg,
.m3-icon-btn:not(.m3-icon-btn--standard):hover svg,
.m3-chip:hover .m3-chip__icon,
.m3-chip:hover svg,
.m3-select__trigger:hover .m3-select__icon,
.m3-select__trigger:hover .m3-select__label--placeholder,
.m3-select__trigger:hover > svg,
.m3-fab:hover .m3-fab__icon,
.m3-fab:hover svg,
.m3-segmented__btn:hover svg,
.m3-tab--active:hover .m3-tab__icon,
.m3-tab--active:hover .m3-tab__label,
.m3-tab--active:hover svg,
.m3-select--open .m3-select__trigger .m3-select__icon,
.m3-select--open .m3-select__trigger .m3-select__label--placeholder,
.m3-select--open .m3-select__trigger > svg,
.m3-select__trigger.m3-select--open .m3-select__icon,
.m3-select__trigger.m3-select--open .m3-select__label--placeholder,
.m3-select__trigger.m3-select--open > svg,
.m3-select__trigger--filled .m3-select__icon,
.m3-select__trigger--filled .m3-select__label--placeholder,
.m3-select__trigger--filled > svg {
  color: var(--sys-zinc-100);
}
/* Standard icon-btn hover — transparent bg, glyph lifts to canonical
   bluepurple-300 hex (theme-stable per design spec).  Descendant SVGs
   inherit the same color so the entire glyph (including any inline
   icon paths) reads as the dominant highlight tone on hover. */
.m3-icon-btn--standard:hover svg {
  color: var(--brand-stable-300);
}


}

/* ── ui-kit.components: src/styles/components/skeleton.css ── */
@layer ui-kit.components {
/* ── Skeleton shimmer system: .m3-skeleton + composed CardSkeleton/ListRowSkeleton/SkeletonOverlay/DropdownSkeleton ─ */

/* ── Phase 4b.3 excision: live-packets page (pkt-* part 1: page → column visibility popover) moved to `frontend/src/styles/pages/live-packets.css`. */
/* ── Skeleton shimmer system ──
   Canonical shimmer recipe consumed by <Skeleton> in
   `components/material/Skeleton.tsx` AND by every legacy selector that
   shipped its own gradient (`.m3-dropdown-skeleton__chip`,
   `.edge-shelf-packets__skeleton-row`).  ONE keyframe, ONE recipe,
   ONE reduced-motion guard.  When introducing a new skeleton site
   reach for `<Skeleton variant="chip|row|block" />` first; only add a
   new alias selector if you have a back-compat constraint that
   prevents touching the TSX.

   Recipe (industry-standard base + wave — Material UI /
   react-loading-skeleton pattern):
     1. The element paints a SOLID muted base color (`--surface-tint-2`).
        This is the always-visible "loading slot" silhouette.
     2. A `::after` pseudo-element absolutely positions itself over
        the box and translates a translucent highlight band
        (`--surface-tint-4`) from left to right on a linear loop.

   Why two layers (not the old single-gradient form): the previous
   recipe interpolated `--surface` → `--surface-hover` → `--surface`,
   but `--surface-hover` resolves to `transparent` in EVERY theme
   (`:root`, dark, dim, light all set it that way).  So on any parent
   that already painted `--surface` (i.e. every canonical card),
   the skeleton appeared as a static rectangle with no visible motion;
   the "shimmer" was just transparent-over-the-same-color.  The solid
   base + translucent wave below is theme-stable, parent-agnostic, and
   visible on every surface in the chrome roster.

   Variants:
     • `--chip`  — inline shimmer span.  Label / badge / micro-stat
                   placeholder where the surrounding flow sizes the box.
     • `--row`   — single full-width shimmer line.  Default 16 px tall.
     • `--block` — sized shimmer rectangle.  Chart-card body slots,
                   image placeholders, anything needing explicit W×H. */
/* ── Canonical base + wave skeleton recipe ────────────────────────
   Industry-standard recipe lifted verbatim from Material UI /
   react-loading-skeleton:
     1. The element paints a SOLID muted base color
        (`--surface-tint-2`) so the silhouette is always visible.
     2. A `::after` pseudo-element absolutely covers the box and
        translates a translucent highlight band (`--surface-tint-4`)
        from left to right via the `m3-skeleton-wave` keyframe below.

   Pure CSS — no JS frame baker, no per-instance custom properties,
   no data-URI swaps, no `requestAnimationFrame` loop.  The browser
   compositor handles the translate animation entirely off the main
   thread; consumer count is no longer a frame-budget concern.

   This replaces the previous "Pixel Static" 24-frame procedural
   noise system.  That recipe baked 24 SVG data URIs into `:root`
   custom properties and cycled them via `step-end` keyframes; an
   rAF loop in `lib/skeletonStatic.ts` re-baked one slot every ~83 ms
   so the field read as never-repeating.  Each slot rewrite called
   `documentElement.style.setProperty('--skel-frame-N', ...)` which
   invalidated computed style across every `.m3-skeleton` descendant
   (the keyframe reads the var via `var()`), forcing tree-wide
   re-decode of a multi-KB data URI 12 times a second.  On dense
   pages (Network Stats stamped 30+ skeleton consumers) that
   dominated the frame budget and stalled the main thread.  See
   `lib/skeletonStatic.ts`'s history for the full perf rationale.

   `--surface-tint-2` / `--surface-tint-4` are per-theme tokens (see
   the `:root[data-theme="..."]` blocks above), so the skeleton
   pivots automatically with the theme without any per-mode rules.

   Containment: `overflow: hidden` + `border-radius` clips the wave
   to the consumer's rounded-rect.  `--shape-xs` is the default;
   `.m3-skeleton--block` upgrades to `--shape-sm` for larger
   placeholders (chart bodies, image slots).

   `prefers-reduced-motion: reduce` halts the wave — the solid base
   color stays visible as a static silhouette. */
.m3-skeleton {
  position: relative;
  display: inline-block;
  pointer-events: none;
  overflow: hidden;
  border-radius: var(--shape-xs);
  background-color: var(--surface-tint-2);
}
.m3-skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--surface-tint-4) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: m3-skeleton-wave 1.6s ease-in-out infinite;
}
.m3-skeleton--row,
.m3-skeleton--block {
  display: block;
}
.m3-skeleton--block {
  border-radius: var(--shape-sm);
}
/* Single translate keyframe — the wave slides the translucent band
   from `translateX(-100%)` (off-screen left) to `translateX(100%)`
   (off-screen right) once per loop.  Pure compositor-driven
   `transform` animation: no layout, no paint thrash, no main-thread
   work after the initial layer promotion. */
@keyframes m3-skeleton-wave {
  100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .m3-skeleton::after { animation: none; }
}

/* ── Composed: CardSkeleton ──
   Layout-only — chrome comes from whichever roster alias the consumer
   passes via `cardClass` (`.m3-card`, `.feature-card`, `.pkt-card`,
   `.edge-shelf-disclosure`, `.connect-card`, `.connect-section`,
   `.net-rank-row`, `.nd-status-card`).  This rule paints internal
   padding + the row stack; the chrome roster rule above paints the
   bevel / shadow / radius. */
.m3-card-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--card-pad);
}
.m3-card-skeleton__header {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}

/* ── Composed: ListRowSkeleton ──
   Multi-cell shimmer row.  Each cell takes its flex-basis from inline
   `style` (the JSX side sets `flex: N` per the consumer-supplied
   `columns` array), so the column rhythm is parameterised at the
   call site rather than the recipe. */
.m3-list-row-skeleton {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
}
.m3-list-row-skeleton + .m3-list-row-skeleton {
  border-top: 1px solid var(--border-subtle);
}

/* ── Composed: SkeletonOverlay (hydrate / inflate wrapper) ──
   Floats a shimmer wash over an existing real container so its chrome
   can stay in place during fetch.  See
   `components/material/SkeletonOverlay.tsx` for the contract.

   Motion handles enter/exit opacity via the canonical `skeletonEnter`
   / `skeletonExit` transitions (dramatic quart easings, no springs);
   CSS owns the gradient + radius inheritance + reduced-motion guard.

   `.m3-skeleton-host` only carries `position: relative` (set inline)
   so the wash anchors to its host card.  `.m3-skeleton-overlay`
   covers the entire host via `inset: 0`; the gradient itself uses
   the same `--surface` ↔ `--surface-hover` sweep as the chip / row
   / block variants so every skeleton surface in the app reads as one
   recipe.  `pointer-events: none` lets the masked content stay
   accessible underneath (consumers can still focus-trap or interact
   with real controls behind the wash if needed).

   Intensity modifiers:
     - `--wash` (default) ghosts the host's chrome through at ~85 %
       opacity — reads as "warming up".
     - `--mask` is fully opaque — the underlying dummy values stay
       hidden until the wash fades out on `loading=false`. */
.m3-skeleton-overlay {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
  /* Canonical base + wave recipe — see `.m3-skeleton` above for the
     full rationale.  Solid base color via `--surface-tint-2`, wave
     via the `::after` band below.  Overflow stays hidden + border-
     radius inherits from the host so the wave never bleeds past the
     host card's rounded corner. */
  overflow: hidden;
  border-radius: inherit;
  background-color: var(--surface-tint-2);
}
.m3-skeleton-overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--surface-tint-4) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: m3-skeleton-wave 1.6s ease-in-out infinite;
}
.m3-skeleton-overlay--wash {
  opacity: 0.85;
}
.m3-skeleton-overlay--mask {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .m3-skeleton-overlay::after { animation: none; }
}

/* ── Dropdown skeleton rows ──
   Used by <DropdownSkeletonRows> when there is *no* prior result list to
   keep on screen during loading (initial open / first query crossing a
   minLength threshold).  When stale results are available, prefer the
   thin progress strip above instead — flicker-free for fast filters.

   The `__chip` selector is retained for back-compat with the existing
   DropdownSkeletonRows markup, but the chip recipe itself routes
   through the canonical `.m3-skeleton--chip` (above) so the gradient +
   keyframe + reduced-motion guard live in one place.  Only the layout
   modifiers (`--code` / `--name` / `--tail`) stay local to this block. */
.m3-dropdown-skeleton {
  contain: content;
}
.m3-dropdown-skeleton__row {
  pointer-events: none;
}
.m3-dropdown-skeleton__chip {
  /* Canonical base + wave recipe — same shimmer family as
     `.m3-skeleton` above; the legacy selector is kept so the
     existing DropdownSkeletonRows markup keeps animating without a
     TSX edit.  Rounded-rect clipping via `--shape-xs` + `overflow:
     hidden` matches the canonical recipe. */
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: var(--shape-xs);
  height: 12px;
  background-color: var(--surface-tint-2);
}
.m3-dropdown-skeleton__chip::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--surface-tint-4) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: m3-skeleton-wave 1.6s ease-in-out infinite;
}
.m3-dropdown-skeleton__chip--code { width: 32px; flex-shrink: 0; }
.m3-dropdown-skeleton__chip--name { flex: 1; margin: 0 var(--sp-1); min-width: 0; }
.m3-dropdown-skeleton__chip--tail { width: 24px; flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) {
  .m3-dropdown-skeleton__chip::after { animation: none; }
}


}

/* ── ui-kit.components: src/styles/components/dual-slider.css ── */
@layer ui-kit.components {
/* ── M3 Dual Range Slider (M3 2024 spec) ────────────────────────────────── */

/* ═══ M3 Dual Range Slider (M3 2024 spec) ═══════════════════════════════════ */
/* Two-handle time-range slider — thick segmented tracks with handle
   gaps, stop indicators, value indicator popup, and full rounding
   on track termini. All dimensions via scoped --_sl-* tokens so
   consumers can override via style or parent selectors.               */

.m3-dual-slider {
  /* ── Configurable tokens (override via style attr or ancestor) ── */
  --_sl-gap:              var(--sp-1_5);     /* handle-to-track gap                */
  --_sl-track-h:          12px;    /* track segment thickness            */
  --_sl-touch:            48px;    /* invisible touch target per handle  */
  --_sl-handle-w:         4px;     /* handle capsule width (idle)        */
  --_sl-handle-h:         44px;    /* handle capsule height (idle)       */
  --_sl-handle-w-hover:   6px;     /* handle width on hover              */
  --_sl-handle-w-press:   2px;     /* handle width while pressed/drag    */
  --_sl-handle-h-press:   var(--_sl-handle-h); /* height stays same   */
  --_sl-handle-inset:     calc(var(--_sl-track-h) / 2); /* padding from track terminus to handle center (= border-radius) */
  --_sl-stop:             4px;     /* stop indicator diameter             */
  --_sl-stop-inset:       var(--_sl-gap); /* stop dot inset from seg edge */
  --_sl-indicator-size:   40px;    /* value indicator pill height+min-w  */
  --_sl-indicator-offset: 28px;    /* indicator distance above track ctr */
  --_sl-color-active:     var(--accent);
  --_sl-color-active-hov: var(--accent-dark);
  --_sl-color-inactive:   var(--surface-active);
  --_sl-color-handle:     var(--accent);
  --_sl-color-handle-idle:var(--text-muted);
  --_sl-color-stop:       var(--bg);
  --_sl-color-ind-bg:     var(--accent-subtle);   /* tonal indigo bg    */
  /* --text-accent = bluepurple-200 (dark/dim) / bluepurple-600 (light): passes WCAG AA for value indicator text. */
  --_sl-color-ind-text:   var(--text-accent);

  display: flex;
  flex-direction: column;
  gap: var(--sp-0_5);
  min-width: 160px;
  flex: 1;
  overflow: visible;
}

/* ── Labels ── */

.m3-dual-slider__labels {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  letter-spacing: var(--ls-label-sm);
  font-family: var(--mono);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  user-select: none;
  -webkit-user-select: none;
}

.m3-dual-slider__label--active {
  /* Use --text-accent rather than --_sl-color-active (--accent) so the
     active-bound label passes WCAG AA; the track fill keeps --_sl-color-active. */
  color: var(--text-accent);
}

/* ── Track container ── */

.m3-dual-slider__track {
  position: relative;
  height: var(--_sl-touch);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
  overflow: visible;
}

/* ── Track segments (thick pill-rounded bars, inset from edges) ── */

.m3-dual-slider__seg {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  height: var(--_sl-track-h);
  border-radius: var(--shape-full);
  pointer-events: none;
}

.m3-dual-slider__seg--inactive {
  background: var(--_sl-color-inactive);
}

.m3-dual-slider__seg--active {
  background: var(--_sl-color-active);
  pointer-events: auto;
  cursor: grab;
  transition: background var(--dur-short-2) var(--ease-standard);
}
.m3-dual-slider__seg--active:hover {
  background: var(--_sl-color-active-hov);
}
.m3-dual-slider__seg--dragging {
  cursor: grabbing;
  background: var(--_sl-color-active-hov);
}

/* ── Stop indicators (dots at inactive segment inner edges) ── */

.m3-dual-slider__stop {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: var(--_sl-stop);
  height: var(--_sl-stop);
  border-radius: var(--shape-full);
  background: var(--_sl-color-stop);
  pointer-events: none;
}
.m3-dual-slider__stop--start { left: var(--_sl-stop-inset); }
.m3-dual-slider__stop--end   { right: var(--_sl-stop-inset); }

/* ── Value indicator (tonal indigo pill above handle during drag) ── */

.m3-dual-slider__indicator {
  position: absolute;
  bottom: calc(50% + var(--_sl-indicator-offset));
  transform: translateX(-50%);
  min-width: var(--_sl-indicator-size);
  height: var(--_sl-indicator-size);
  padding-inline: var(--sp-2);
  border-radius: var(--shape-full);
  background: var(--_sl-color-ind-bg);
  color: var(--_sl-color-ind-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-label-sm);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-label-sm);
  white-space: nowrap;
  box-shadow: var(--elevation-2);
  pointer-events: none;
  z-index: 4;
  animation: m3-slider-indicator-in var(--dur-short-2) var(--ease-emphasized-decel);
}

@keyframes m3-slider-indicator-in {
  from { transform: translateX(-50%) scale(0.6); opacity: 0; }
  to   { transform: translateX(-50%) scale(1);   opacity: 1; }
}

/* ── Handle (vertical capsule — compresses on press per M3 2024) ── */

.m3-dual-slider__handle {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: var(--_sl-handle-w);
  height: var(--_sl-handle-h);
  border-radius: var(--shape-full);
  background: var(--_sl-color-handle-idle);
  outline: none;
  touch-action: none;
  cursor: ew-resize;
  transition: width var(--dur-short-2) var(--ease-standard),
              height var(--dur-short-2) var(--ease-standard),
              background var(--dur-short-2) var(--ease-standard),
              box-shadow var(--dur-short-2) var(--ease-standard);
  z-index: 2;
}

/* Expanded touch target — invisible hit area */
.m3-dual-slider__handle::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--_sl-touch);
  height: var(--_sl-touch);
}

.m3-dual-slider__handle:hover {
  background: var(--_sl-color-handle);
  width: var(--_sl-handle-w-hover);
}

.m3-dual-slider__handle:focus-visible {
  background: var(--_sl-color-handle);
  box-shadow: 0 0 0 8px var(--accent-subtle);
}

/* Active handle (range is constrained) */
.m3-dual-slider__handle--active {
  background: var(--_sl-color-handle);
}
.m3-dual-slider__handle--active:hover {
  width: var(--_sl-handle-w-hover);
}

/* Dragging — M3 pressed state: capsule thins to suggest precision,
   no transition for zero-latency tracking, elevated z.
   Explicit :hover override so the 2dp width wins over :hover's 6dp. */
.m3-dual-slider__handle--dragging,
.m3-dual-slider__handle--dragging:hover {
  cursor: grabbing;
  transition: none;
  will-change: left;
  background: var(--_sl-color-handle);
  width: var(--_sl-handle-w-press);
  height: var(--_sl-handle-h-press);
  z-index: 3;
}


}

/* ── ui-kit.components: src/styles/components/bento.css ── */
@layer ui-kit.components {
/* ── .bento-grid + .bento-card (canonical sparkline-card tile grid) ─────── */

/* ═══ .bento-grid — canonical sparkline-card tile grid ══════════════════════
   2-column tile grid for "hero number + delta + sparkline" cards.  Each
   tile carries `.bento-card` for layout + a canonical card chrome alias
   (`.m3-card`, `.feature-card`, …) for the raised-paper treatment, so
   the grid is purely positional and the cards inherit the same chrome
   contract as the rest of the unified card system.
   Override the column count via the `--bento-cols` custom property
   when a section needs a wider grid.  Stacks to one column on Compact
   / Medium (≤ 899 px) so tiles full-bleed on phones.
   Consumers today: NodeComposition (Repeaters / Room Servers / Other
   / Total Nodes) and NodePacketHealth (Transport / Multibyte /
   Duplicates / Forwarded) — both 2×2. */
.bento-grid {
  display: grid;
  grid-template-columns: repeat(var(--bento-cols, 2), 1fr);
  gap: var(--sp-4);
}

@media (max-width: 899px) {
  .bento-grid {
    grid-template-columns: 1fr;
  }
}

/* ── .bento-card — hero number + delta + sparkline tile ─────────────────
   Layout-only — chrome (radius, bevel, shadow) is painted by the
   consumer's canonical card alias (e.g. `.feature-card` or `.m3-card`).
   Bottom-aligns the sparkline so every tile in a row shares one
   sparkline baseline regardless of headline height. */
.bento-card {
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.bento-card__content {
  padding: var(--sp-4) var(--sp-4) 0;
}

/* ── .bento-card--fill — adaptive sparkline height ──────────────────────
   Opt-in modifier (applied by `<BentoCard fillHeight />`) that lets
   the sparkline absorb any extra vertical space the tile's grid cell
   picks up from a stretched row.  Use case: a `.bento-grid` row
   mixing 3 sparkline tiles (e.g. Observers / Regions / Packets at
   height=36) with one taller multi-line tile (Top Regions at
   chartHeight=64) — CSS-Grid's default `align-items: stretch` makes
   every cell match the tallest, so without this modifier the 3
   sparkline tiles sit with 28+ px of empty space between the
   headline and the bottom-anchored sparkline (`justify-content:
   space-between`).  With this modifier, the headline pins to its
   natural height and the sparkline grows to claim the empty band,
   yielding a visually balanced row.

   The contract is split:
     • CSS (this rule) pins `.bento-card__content` to its natural
       height (`flex: 0 0 auto`) so the headline never tries to
       absorb extra vertical space when the parent stretches.
     • `<MiniUPlotSparkline fillHeight />` inline-styles the
       sparkline container as `flex: 1 1 <height>px; min-height:
       <height>` so it floors at the prop's default height and grows.
   Both halves are required: the CSS alone wouldn't override the
   sparkline's pinned inline `height: <height>` style; the inline
   alone wouldn't constrain the headline column from absorbing space.

   `min-height: 0` on `.bento-card--fill` itself ensures the flex
   item's intrinsic min-height (the sum of its children's minimums)
   doesn't prevent the card from shrinking to its grid cell when the
   row is shorter than the children's natural total. */
.bento-card--fill {
  min-height: 0;
}
.bento-card--fill > .bento-card__content {
  flex: 0 0 auto;
}

/* Tile label ("Repeaters", "Transport", …) — the eyebrow above the
   hero value.  Margin-bottom is HALF the `--sp-1_5` (6 px) half-step
   (3 px) — no named token sits between `--sp-0_5` (2 px) and `--sp-1`
   (4 px), so this stays anchored to `--sp-1_5` via `calc()` rather
   than picking a coincidentally-close but semantically unrelated
   step; the eyebrow now sits closer still to the hero value, reading
   as one tight stack. */
.bento-card__label {
  font-family: var(--mono);
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  letter-spacing: 0.08em;
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: calc(var(--sp-1_5) / 2);
}

/* Hero row — value + momentum delta on the same typographic baseline. */
.bento-card__headline {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

/* Hero value — snaps to the `--fs-headline-sm` ladder step (24 px)
   so the kit's headline scale owns this stat-tile-style number.  If
   the design later needs a true 26 px peg, add `--fs-stat-hero` to
   `theme-extras.css` and migrate this rule + any equivalents. */
.bento-card__value {
  font-size: var(--fs-headline-sm);
  font-family: var(--mono);
  font-weight: var(--fw-medium);
  color: var(--text);
  line-height: 1;
}

.bento-card__delta {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-1);
  font-size: var(--fs-title-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}

/* Trend arrow stays vertically centered against the percentage text
   while the parent row aligns the % digits to the value's baseline. */
.bento-card__delta > svg {
  align-self: center;
  flex-shrink: 0;
}


}

/* ── ui-kit.components: src/styles/components/chart.css ── */
@layer ui-kit.components {
/* ── Chart chrome: WaevChart widget + canonical chart legend + canonical chart card ─ */

/* ═══════════════════════════════════════════════════════════════════
   WaevChart — M3 chart container widget
   ═══════════════════════════════════════════════════════════════════ */

.wc-container {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--sp-4);
}

.wc-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-3);
  flex-shrink: 0;
}

.wc-chart-zone {
  flex: 1;
  min-height: 0;
  position: relative;
}

.wc-y-axis {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 20px; /* X_AXIS_HEIGHT */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  padding-top: var(--sp-1_5);
  padding-bottom: var(--sp-1_5);
}

/* Y-axis frame — full rounded rect behind the data well, extends further left */
.wc-y-frame {
  position: absolute;
  border: 1px solid var(--border);
  border-radius: var(--shape-sm);
  background: transparent;
}

.wc-y-label {
  padding-right: var(--sp-2);
  padding-left: var(--sp-2);
  font-family: var(--mono);
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  color: var(--text-tertiary);
  white-space: nowrap;
  pointer-events: none;
}

.wc-well {
  position: absolute;
  overflow: hidden;
  border-radius: var(--shape-sm);
  border: 1px solid var(--border);
  z-index: 1; /* sits on top of wc-y-frame */
}

.wc-canvas {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
}

.wc-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-family: var(--mono);
  font-size: var(--fs-label-sm);
  color: var(--text-tertiary);
}

.wc-x-axis {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  pointer-events: none;
  padding-top: var(--sp-1);
}

.wc-x-label {
  position: absolute;
  font-family: var(--mono);
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  color: var(--text-tertiary);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 599px) {
  .wc-x-label--mobile-hidden { display: none; }
}

/* ── Hidden OS pointer for red-cursor charts ───────────────
   Design-system-wide rule: any chart that paints the canonical
   ind-red canvas cursor (solid red follower line + 10% halo band +
   red wedge markers from `drawCursorLine` / `drawWedgeMarker`)
   hides the OS pointer over its hover surface — the red indicator
   IS the cursor, and the arrow/crosshair on top reads as a second
   competing pointer.

   The value is a fully-TRANSPARENT 32×32 PNG, NOT `cursor: none`.
   `none` is special-cased by the compositor as "hide the cursor",
   and during fast pointer movement the OS paints the hardware
   cursor for a frame before the hide re-resolves for the element
   being entered — the arrow flickers back over the chart on quick
   moves.  A transparent cursor IMAGE is a real cursor the browser
   always draws (just invisible), so there is no hide↔show
   transition left to flicker.  `none` stays as the comma fallback
   if the data-URI ever fails to decode.  (PNG, not SVG — Safari
   has no SVG-cursor support.)  Same recipe the live-map globe card
   uses for its canvas-drawn crosshair.

   Consumers: WaevChart (via WaevChartShell `hidePointer`),
   WaevNoiseFloorChart, MiniUPlotSparkline, MultiSparklineCard,
   PercentileSparklineCard, NodePacketsChart, PayloadMixChart,
   HopDistributionChart.  Bar-family charts (stacked / mirrored)
   keep their crosshair — they indicate hover via bucket dimming,
   not the red cursor. */
.wc-cursor-hide,
.wc-cursor-hide * {
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGklEQVR4nO3BAQEAAACCIP+vbkhAAQAAAO8GECAAARlDNO4AAAAASUVORK5CYII=") 16 16, none;
}

/* Cursor-bound tooltip badge.  Hangs from the top edge of the data
   well, anchored to the cursor's CSS-pixel X via inline `left` set
   from the hover handler.  Default placement sits to the RIGHT of
   the dashed cursor line with `CURSOR_TOOLTIP_PADDING_PX` of breathing
   room (also set inline as `transform: translateX(8px)`); the hover
   handler swaps the transform to a mirrored `translateX(calc(-100% -
   8px))` when the badge would overflow the well's right edge so it
   never gets clipped by the well's `overflow: hidden`.

   Typography matches `.wc-x-label` (mono, label-xs) so the cursor's
   readout reads in the same convention as the x-axis ticks directly
   underneath it.  Chip chrome borrows from `.legend-chip`
   (`--surface-tint-3` background, `--shape-2xs` radius) for visual
   consistency with the chart legends. */
.wc-cursor-tooltip {
  position: absolute;
  top: 8px;
  left: 0;
  /* `pointer-events: none` keeps the badge from intercepting the
     mouse so hover stays smooth as the cursor passes underneath it. */
  pointer-events: none;
  /* z-index: 2 sits above the canvas (z-index 1 implicit on .wc-well
     children) and any downtime overlay layers. */
  z-index: 2;
  font-family: var(--mono);
  font-size: var(--fs-label-xs);
  line-height: var(--lh-label-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  background: var(--surface-tint-3);
  -webkit-backdrop-filter: blur(7px); /* Safari */
  backdrop-filter: blur(7px);
  padding: var(--sp-0_5) var(--sp-2);
  border-radius: var(--shape-xs);
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--dur-short-2) var(--ease-standard);
}

.wc-footer {
  padding: var(--sp-2) var(--sp-3) 0; /* extra X padding to match pkt-card-header sp-6 total (sp-3 container + sp-3 footer = sp-6) */
  flex-shrink: 0;
}

/* ── Canonical chart legend (.legend*) ──────────────────────────
   Single source of truth for every legend in the app. Four
   orthogonal axes drive the visual:
     * density  : compact (default) | spacious  (--legend--spacious)
     * chrome   : chip    (.legend-chip wrapper) | bare (no wrapper)
     * emphasis : strong  (--strong on label)    | muted (default)
     * layout   : inline  (default flex-wrap)    | grid  (--grid)
   Density flips via CSS custom props on the container so modifiers
   just override the tier instead of duplicating rules. Dot color is
   data-driven so it stays inline (`style={{ background }}`); every
   other property is class-driven from this file. */

.legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  overflow: hidden;
  font-family: var(--mono);
  /* Compact density defaults — the only density still in use after
     the spacious-footer retirement. Custom props are kept here (rather
     than hard-coded values) so any future re-tuning is a one-knob
     change, and so non-component consumers (e.g. NoiseFloorCard
     StatRow) can locally override a single token if needed. */
  /* Raised from label-xs (10 px) to label-sm (11 px) so the mono glyph
     fills the chip height more completely while staying within the
     chip's 1 px top + bottom padding.  One typescale step = one point
     in this scale.  All legend consumers (payload mix, hop distribution,
     etc.) inherit the bump via this single custom-property assignment. */
  --legend-fs: var(--fs-label-sm);
  --legend-lh: var(--lh-label-sm);
  --legend-gap: var(--sp-3);
  --legend-item-gap: var(--sp-1);
  --legend-swatch-size: var(--sp-2);
  --legend-swatch-shape: var(--shape-3xs);
  --legend-dim-opacity: 0.35;
  font-size: var(--legend-fs);
  line-height: var(--legend-lh);
  /* Horizontal hitbox gaplessness: the container provides only the
     ROW gap (vertical rhythm when entries wrap); horizontal spacing
     between siblings lives INSIDE each item's hitbox via the
     `padding-inline-end` rule on `.legend > .legend-item:not(:last-child)`
     below.  Without this, a container `column-gap` opens a dead-zone
     between items where `onMouseLeave` on item N fires before
     `onMouseEnter` on item N+1, dropping the chart's highlight state
     for a frame and reading as a quick "flicker to black" as the
     non-hovered series momentarily un-dim.  Splitting the gap
     dimensions into `row-gap` (kept) and `column-gap: 0` (zeroed)
     keeps the multi-row layout while making adjacent items share an
     edge horizontally. */
  row-gap: var(--legend-gap);
  column-gap: 0;
}

.legend--grid {
  display: grid;
  grid-template-columns: auto auto;
  /* Grid retains zero column gap (matches the inline layout's hitbox-
     gapless contract above so the per-item `padding-inline-end` rule
     carries all horizontal spacing); `--sp-0_5` (2 px) row gap is the
     tight vertical rhythm the grid legend uses by default. */
  row-gap: var(--sp-0_5);
  column-gap: 0;
  flex-wrap: initial;
}

/* ── Grid legends in compact chart-card headers: wrap, don't crush ──
   The compact header lays out `[title | legend]` on one flex row.  The
   legend chips' default shrink contract (`flex-shrink: 1; min-width: 0;
   overflow: hidden`) lets labels ellipsize to unreadable stubs ("M.",
   "LAX…") whenever the row is tight — and because the chips CAN
   shrink, the flex line never overflows, so `flex-wrap` alone never
   fires.  Inside compact headers, make grid-legend labels unshrinkable
   (no ellipsis, no chip shrink) so the legend's min-content width is
   its full natural width — when that doesn't fit beside the title, the
   whole legend wraps onto its own row below the title instead of
   crushing.  Applies at every viewport width; the ≤599px block below
   additionally stretches the wrapped grid to two equal columns for a
   cleaner phone read. */
.chart-card--compact .chart-card__header {
  flex-wrap: wrap;
  /* Nudge items to the top so a tall multi-line legend (layout="grid")
     doesn't shift the title vs neighbouring tiles in a 3-up row. */
  align-items: flex-start;
}
.chart-card--compact .chart-card__header .legend--grid .legend-item {
  /* Override the base `.legend-item { min-width: 0 }` — with 0 the
     grid's `auto` tracks may collapse below content and the row CLIPS
     instead of wrapping.  `max-content` makes each item report its
     full natural width up through the grid to the header's flex
     layout, so the wrap actually fires when space runs out. */
  min-width: max-content;
}
.chart-card--compact .chart-card__header .legend--grid .legend-chip {
  flex-shrink: 0;
  overflow: visible;
}
.chart-card--compact .chart-card__header .legend--grid .legend-label {
  /* Last-resort guard: labels are bounded identities (e.g. mesh
     display names — see Top Regions' `meshDisplayName` labels), so
     they normally render in full.  The 18ch cap + base ellipsis only
     bites on pathological names, keeping the no-crush wrap math
     bounded instead of letting one rogue label blow out the row. */
  max-width: 18ch;
}

/* ── Mobile: wrapped grid legends stretch to a full-width 2-col row ──
   At phone width the wrapped legend additionally takes 100% of the
   header row split into two equal `1fr` columns, with each cell's
   value pushed to the right edge (`justify-content: space-between`) so
   the label–value pairs read as aligned columns. */
@media (max-width: 599px) {
  .chart-card--compact .chart-card__header .legend--grid {
    width: 100%;
    grid-template-columns: 1fr 1fr;
  }
  .chart-card--compact .chart-card__header .legend--grid .legend-item {
    justify-content: space-between;
  }
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--legend-item-gap);
  min-width: 0;
  transition: opacity var(--dur-short-2) var(--ease-standard);
}
/* Visual horizontal gap between siblings lives INSIDE each item's
   hitbox via `padding-inline-end` so the cursor never crosses a dead
   pixel between items — see the `.legend` docstring above for the
   flicker-prevention rationale.  Scoped to direct children of
   `.legend` so standalone `.legend-item` usages (e.g. NoiseFloorCard
   StatRow stack, where items live in a flex-column and rely on their
   natural intrinsic width) keep their original measured width. */
.legend > .legend-item:not(:last-child) {
  padding-inline-end: var(--legend-gap);
}
.legend-item[data-interactive="true"] { cursor: pointer; }
.legend-item--dimmed { opacity: var(--legend-dim-opacity); }

.legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px var(--sp-1) 1px 3px;
  border-radius: var(--shape-2xs);
  /* `--surface-tint-3` (white-8 in dark/dim, black-8 in light) is the
     theme-aware token that matches the legacy hand-rolled
     `rgba(255, 255, 255, 0.10)` chip background while also rendering
     correctly on light surfaces. */
  background: var(--surface-tint-3);
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  transition: background var(--dur-short-2) var(--ease-standard);
}
.legend-chip--selected {
  background: var(--accent-subtle);
}

.legend-swatch {
  width: var(--legend-swatch-size);
  height: var(--legend-swatch-size);
  border-radius: var(--legend-swatch-shape);
  flex-shrink: 0;
}

.legend-label {
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.legend-label--strong {
  font-weight: var(--fw-medium);
  /* Note: uppercase transformation is handled in JS by `<Legend>`
     so labels with intentional casing (e.g. "0x0D" hex payload
     types) keep their lowercase prefix.  Don't move that to CSS
     `text-transform: uppercase` — it would clobber the "x". */
}

.legend-value {
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex-shrink: 0;
}
/* When the value sits flush against a `.legend-chip` the chip edge is
   the visual separator — pad only that case so bare layouts retain
   their natural `--legend-item-gap` spacing.

   `min-width: 2rem` (32 px) gives every value slot a rigid fixed boundary
   so the chip layout doesn't shift ("bounce") when the value cycles
   between short strings ("0%", "1h") and longer ones ("28%", "17.5K").
   `text-align: left` tucks the text immediately after the chip rather
   than pushing it to the far right edge of the slot — consumers that
   previously applied `textAlign: "right"` inline should drop that
   override since left is now the canonical alignment. */
.legend-chip + .legend-value {
  padding-left: var(--sp-0_5);
  min-width: 2rem;
  text-align: left;
}

/* ── Canonical chart card (.chart-card*) ────────────────────
   Single source of truth for the "title + legend + chart body"
   container shared by every chart in the app.  Three density tiers
   driven by CSS custom props (`--chart-card-header-pad`,
   `--chart-card-body-height`) so modifiers flip the tier instead of
   duplicating rules:

     * `large`    — header lives OUTSIDE a `.nd-chart-card` data well.
                    No outer chrome; visual definition comes from the
                    canvas-drawn keylines inside the data well.  Used
                    by Outpost / LivePackets `Packets Received` and
                    NodeDetail's Packet Analyzer cards.
     * `compact`  — header lives INSIDE the card chrome; the consumer
                    composes `.feature-card` for raised-paper chrome.
                    Used by every sparkline card (NodePacketsChart,
                    PercentileSparklineCard, MultiSparklineCard).
     * `well`     — self-contained chart card.  No outer chrome, no
                    ChartCard header (the inner `<WaevChart>` paints
                    its own header/footer inside the data well).  Body
                    composes `.nd-chart-card` and pins to a 280 px
                    floor so the WaevChart's flex shell has space to
                    distribute (NodePacketAnalyzer, NodeNoiseFloor).

   Header typography (body-md muted title + flex-between layout) is
   shared via `.chart-card__header` and `.chart-card__title` so both
   densities read with one voice.  The dot-data-driven legend slot
   is rendered as a sibling node — ChartCard does not own the legend
   styling, only its layout position. */

.chart-card {
  display: flex;
  flex-direction: column;
  /* `justify-content: space-between` pins the header to the top of the
     outer card and the body to the bottom — same flex contract
     NoiseFloorCard's `<Pane>` uses.  When the outer card is stretched
     by a parent grid (e.g. `.nd-mini-charts` matching the row height
     of the tallest sibling), any extra vertical space sits BETWEEN
     header and body, not below the body.  That keeps the chart canvas
     anchored to the card's bottom edge regardless of stretch — reads
     edge-to-edge instead of leaving an empty card-background gap
     under the chart. */
  justify-content: space-between;
  min-width: 0;
  /* Density-driven defaults — overridden by --large / --compact. */
  --chart-card-header-pad: 0;
  --chart-card-body-height: auto;
}

.chart-card--large {
  /* Header above the data well; no outer chrome.  Page flow provides
     vertical breathing room via the canonical `--block-gap` token. */
  margin-bottom: var(--block-gap);
  --chart-card-header-pad: 0 0 var(--sp-2);
  --chart-card-body-height: 260px;
}

.chart-card--compact {
  /* Compact tier: composes `.feature-card` chrome on the SAME element.
     `padding: 0` lets the internal header own its own padded slot;
     `overflow: hidden` clips the inner canvas to the rounded corners. */
  padding: 0;
  overflow: hidden;
  --chart-card-header-pad: var(--sp-3) var(--sp-4);
  --chart-card-body-height: 72px;
}

.chart-card--well {
  /* Self-contained chart card: no outer chrome, no ChartCard header.
     The body composes `.nd-chart-card` (which carries its own 280 px
     min-height floor), but `.chart-card__body`'s `min-height: var(...)`
     rule lives later in this file and wins on equal specificity over
     `.nd-chart-card`'s floor.  Set the custom prop to 280 px directly
     so the well body gets the intended floor through the canonical
     property channel — no override rule, no specificity gymnastics.
     Required because `.nd-chart-card .wc-container { flex: 1;
     min-height: 0 }` flattens the inner WaevChart container when the
     `.chart-card__body` ancestor has no enforced height. */
  --chart-card-body-height: 280px;
}

.chart-card__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--chart-card-header-pad);
}
/* When the header renders BELOW the chart body (via
   `headerPosition="below"` on ChartCard), bump the top padding so the
   title + legend row has breathing room from the data well's bottom
   edge.  The body is the immediately-preceding sibling in that layout
   order, so `+` targets exactly the below-body position.  Only fires
   on `large` density (the only tier that uses below-header today). */
.chart-card__body + .chart-card__header {
  padding-top: var(--sp-3);
}

.chart-card__title {
  font-family: var(--mono);
  font-size: var(--fs-label-sm);
  line-height: var(--lh-label-sm);
  letter-spacing: 0.08em;
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  color: var(--text-tertiary);
  flex-shrink: 0;
  min-width: 0;
  white-space: nowrap;
}

.chart-card__body {
  /* `flex: 1 1 var(--chart-card-body-height)` floors the body at the
     density's body-height token but lets it GROW to absorb any extra
     vertical space the outer `.chart-card` picks up from a stretched
     parent grid (e.g. the `.nd-mini-charts` 3-up row, whose track
     stretches every sibling to match the tallest — typically the
     content-heavy `NoiseFloorCard`).  Without flex-grow, the body
     stays at its floor and the stretch-gap sits empty BESIDE it,
     reading visually as padding around an undersized chart canvas.
     With it, the canvas (`width: 100%; height: 100%` of body) fills
     the stretched card edge-to-edge — matching NoiseFloorCard's
     bottom-anchored sparkline contract.

     `large` density composes this body with `.nd-chart-card` so the
     inner `<WaevChart>` inherits the stripped-borders data-well chrome
     (see `.nd-chart-card .wc-y-frame` / `.wc-well` overrides above). */
  flex: 1 1 var(--chart-card-body-height);
  min-height: var(--chart-card-body-height);
}

.chart-card__below {
  padding: 0 var(--sp-4) var(--sp-2);
}

.chart-card__footer {
  padding: var(--sp-2) var(--sp-4);
  flex-shrink: 0;
}

/* ── `.nd-chart-card` data-well variant ───────────────────────────
   Composed onto `.chart-card__body` by ChartCard's `large` and `well`
   densities (see `ChartCard.tsx` density class table).  Strips the
   inner `.wc-container` padding + the `.wc-y-frame` / `.wc-well`
   keyline borders so the chart canvas reads as a minimal data well
   with only the canvas-drawn horizontal y-axis keylines for
   definition — the canonical "full-bleed" chart appearance shared
   across Outpost / LivePackets Packets Received and NodeDetail's
   Packet Analyzer / Noise Floor cards.

   These rules used to live in `frontend/src/styles/pages/node-detail.css`,
   which made them dependent on a page-specific sheet being loaded for
   the canonical chrome to apply.  Hoisted into the kit in Phase 5g so
   `density="large"` always paints edge-to-edge regardless of which
   route imports the page sheets.  The chart-grid color tuning still
   lives at the consumer via the `--chart-grid` token. */
.nd-chart-card {
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  min-height: 280px;
  overflow: hidden;
}

/* Full bleed: chart fills the card edge-to-edge */
.nd-chart-card .wc-container {
  padding: 0;
  flex: 1;
  min-height: 0;
}

.nd-chart-card .wc-header {
  padding: var(--sp-2) var(--sp-3) var(--sp-1) var(--sp-3);
  padding-bottom: var(--sp-1);
  gap: var(--sp-2);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nd-chart-card .wc-footer {
  padding: 0;
}

.nd-chart-card .wc-chart-zone {
  flex: 1;
  min-height: 0;
}

/* Strip the inner chart frame + data-well outlines so the chart
   reads as a minimal data well; only the canvas-drawn keylines
   provide visual definition. */
.nd-chart-card .wc-y-frame,
.nd-chart-card .wc-well {
  border: none;
}

/* Full-bleed context: wc-container has padding:0, so the y-axis sits
   flush against the card's left edge.  Remove the label's left pad so
   the tick text starts at the very edge — right pad is kept so labels
   don't butt up against the data well. */
.nd-chart-card .wc-y-label {
  padding-left: 0;
}


}

/* ── ui-kit.components: src/styles/components/sparkline.css ── */
@layer ui-kit.components {
/* ── MicroSparkline ───────────────────────────────────────────────────
   Tone-to-color mapping for the kit's `<MicroSparkline>` primitive.
   The component's SVG paints in `currentColor`, so the tone class only
   has to set the element's text colour and the polyline + dot inherit.

   Tones map onto the kit's canonical text-* + semantic indicator
   variables so light / dim / dark all paint the same component
   without bespoke per-theme rules.  See `MicroSparkline.tsx` for the
   prop contract.

   Reach for this when you need a dense per-row trend indicator:
   leaderboard sparkline columns, headline + delta pairs, hero-card
   "last 7 days" micro-chart slot. */

.m3-sparkline {
  /* Default tone (accent) — keeps the line visible against any of the
     kit's canonical surface ladders. */
  color: var(--text-accent);
}

.m3-sparkline--accent  { color: var(--text-accent); }
.m3-sparkline--success { color: var(--sys-green-400); }
.m3-sparkline--warning { color: var(--sys-orangeyellow-400); }
.m3-sparkline--danger  { color: var(--sys-red-400); }
.m3-sparkline--info    { color: var(--sys-greenblue-400); }
.m3-sparkline--muted   { color: var(--text-tertiary); }

}

/* ── ui-kit.components: src/styles/components/active-shimmer.css ── */
@layer ui-kit.components {
/* ── ActiveShimmer — CSS-native rotating comet ──────────────────────
   Pure-CSS selection highlight that traces a host's rounded perimeter
   with a rotating indigo comet plus a phase-locked outer glow.  Apply
   `.active-shimmer` to any element with `position: relative` and a
   `border-radius` — no wrapper divs, no SVG overlays, no JavaScript
   sizing, NO size containment (which would break consumers that
   auto-size from content).

   Architecture (single source of truth):
     1. `@property --shimmer-angle` registers an animatable <angle>
        that rotates the conic gradient around the perimeter.
     2. `@property --shimmer-spread` registers an animatable <angle>
        — the half-width of each comet cluster.  Breathes between a
        narrow waist (18°) and a wide apex (88°) over the cycle.
     3. Both registered properties carry `inherits: true` so the
        host's animated values cascade to BOTH pseudo-elements
        automatically — single animation declaration on the host
        keeps the sharp ring and blurred glow in lockstep.
     4. The conic-gradient itself is stored ONCE in a single
        `--shimmer-gradient` custom property on the host element.
        Both `::before` (sharp 1 px ring) and `::after` (blurred
        outer glow) substitute the same gradient string via `var()`,
        so any tweak — peak position, shoulder ramp, centre-out fade
        — propagates to both layers with NO duplication.
     5. `mask-composite: exclude` clips both pseudos to a thin ring
        running just inside the host's border-radius.
     6. `border-radius: inherit` makes both pseudos follow the host's
        corners automatically — no JS radius detection.

   Theme tokens consumed (declared per-theme in `theme-extras.css`):
     --active-shimmer-step-1 … --active-shimmer-step-18
     --active-shimmer-trough

   Customisable via CSS variable overrides on the host element:
     --shimmer-duration:        12s    (rotation / breathing period)
     --shimmer-thickness:        1px   (sharp gradient ring width)
     --shimmer-glow-blur:        8px   (blur radius of the outer glow)
     --shimmer-enter-duration: 700ms   (entrance "announce" pulse)
     --shimmer-leave-duration: 300ms   (exit fade)

   Lifecycle — the shimmer announces itself with a bright entrance
   pulse and fades cleanly on exit:

     Entrance:  When `.active-shimmer` is first applied, the host
                starts at `filter: brightness(2.4)` (via CSS
                `@starting-style`) and transitions DOWN to
                `brightness(1)` over `--shimmer-enter-duration`,
                producing an "I'm here!" announce flash that fades
                into the regular cycle.

     Exit:      Consumer applies `.active-shimmer--leaving` before
                removing the class entirely.  The `opacity` transition
                fades the host from 1 → 0 over `--shimmer-leave-
                duration`.  Once the fade completes, the consumer
                removes both classes to fully unmount the effect.

                React pattern:
                  // 1. set leaving flag
                  setLeaving(true);
                  // 2. wait for the fade duration
                  setTimeout(() => setActive(false), 300);
   ------------------------------------------------------------------- */

/* ── Animatable angle properties ───────────────────────────── */
@property --shimmer-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  /* `inherits: true` so the host's animated value cascades to both
     `::before` (sharp ring) and `::after` (blurred glow). */
  inherits: true;
}

@property --shimmer-spread {
  syntax: "<angle>";
  initial-value: 88deg;
  inherits: true;
}

/* ── Rotation + breathing keyframes ────────────────────────────────
   Two SEPARATE animations share the 12 s timeline so each can carry
   its own timing function without compromising the other:

     1. `shimmer-rotate` walks `--shimmer-angle` linearly 0→360°.
        Linear is required — any easing would visibly speed up /
        slow down the comet's perimeter sweep.

     2. `shimmer-breathe` walks `--shimmer-spread` between a narrow
        18° half-width (each comet ~36° wide — ~20% of perimeter)
        and a wide 88° half-width (each comet ~176° wide — together
        ~98% of perimeter, leaving just two 4° trough notches at
        the conic 90° and 270° angles).  Carries an `ease-in-out`
        curve so the spread visibly RESTS at each apex and
        ACCELERATES through the mid-transition.

   Phase contract — Comet A's peak sits at conic stop 0°, Comet B's
   at 180°, so their perimeter positions are exactly `--shimmer-angle`
   and `--shimmer-angle + 180°`.  Apex moments (--shimmer-spread = 88°)
   coincide with --shimmer-angle = 0° and 180°, where both peaks sit
   dead-centre on the top + bottom horizontal edges of the host.
   Narrow moments (18°) coincide with --shimmer-angle = 90° and 270°,
   where both peaks sit on the short verticals. */
@keyframes shimmer-rotate {
  to { --shimmer-angle: 360deg; }
}

@keyframes shimmer-breathe {
    0% { --shimmer-spread: 88deg; }  /* apex — peaks on horizontals */
   25% { --shimmer-spread: 18deg; }  /* narrow — peaks on verticals */
   50% { --shimmer-spread: 88deg; }  /* apex — peaks swapped */
   75% { --shimmer-spread: 18deg; }  /* narrow — peaks swapped */
  100% { --shimmer-spread: 88deg; }
}

/* ── Core class ──────────────────────────────────────────────────── */

.active-shimmer {
  position: relative;

  /* Tunables — consumers override these on the element to customise. */
  --shimmer-duration:        12s;
  --shimmer-thickness:        1px;
  --shimmer-glow-blur:        8px;
  --shimmer-enter-duration: 700ms;
  --shimmer-leave-duration: 300ms;

  /* Entrance / exit lifecycle channels.  `@starting-style` seeds the
     initial brightness HIGH so the regular value reads as a fade-down
     when the class first applies.  Opacity stays at 1 by default; the
     `--leaving` modifier drops it to 0 to trigger the exit fade. */
  opacity: 1;
  filter: brightness(1);
  transition:
    opacity var(--shimmer-leave-duration) ease-out,
    filter  var(--shimmer-enter-duration) ease-out;

  /* ── Shared conic-gradient definition ─────────────────────────────
     Stored ONCE on the host; both pseudos substitute it via
     `var(--shimmer-gradient)`.  Inner `var(--shimmer-spread)` and
     `var(--shimmer-angle)` references resolve at the substitution
     site against the inherited animated values from the host.

     Peaks at conic stops 0° (Comet A, top) and 180° (Comet B, bottom).
     Wrap-around — Peak A sits at BOTH 0° and 360° so the gradient
     stops express the full bright→trough→bright ring monotonically.

     k-multipliers (0.04, 0.12, 0.20, 0.36, 0.52, 0.68, 0.84, 1.00)
     preserve the centre-out fade shape across whatever half-width
     the breathing animation is currently passing through. */
  --shimmer-gradient: conic-gradient(
    from var(--shimmer-angle),
    /* ── Comet A right shoulder (peak at 0°, fading to trough) ── */
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   0deg,
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-9, var(--sys-bluepurple-300))    calc(var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-7, var(--sys-bluepurple-400))    calc(var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-5, var(--sys-bluepurple-400))    calc(var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-3, var(--sys-bluepurple-500))    calc(var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-1, var(--sys-bluepurple-600))    calc(var(--shimmer-spread) * 0.84),
    var(--active-shimmer-trough, var(--sys-bluepurple-600))    var(--shimmer-spread),
    /* Trough notch centred on 90° (right vertical midpoint). */
    var(--active-shimmer-trough, var(--sys-bluepurple-600))    calc(180deg - var(--shimmer-spread)),
    /* ── Comet B (peak at 180°, shoulders span ±--shimmer-spread) ── */
    var(--active-shimmer-step-1, var(--sys-bluepurple-600))    calc(180deg - var(--shimmer-spread) * 0.84),
    var(--active-shimmer-step-3, var(--sys-bluepurple-500))    calc(180deg - var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-5, var(--sys-bluepurple-400))    calc(180deg - var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-7, var(--sys-bluepurple-400))    calc(180deg - var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-9, var(--sys-bluepurple-300))    calc(180deg - var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(180deg - var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(180deg - var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   180deg,
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(180deg + var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(180deg + var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-9, var(--sys-bluepurple-300))    calc(180deg + var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-7, var(--sys-bluepurple-400))    calc(180deg + var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-5, var(--sys-bluepurple-400))    calc(180deg + var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-3, var(--sys-bluepurple-500))    calc(180deg + var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-1, var(--sys-bluepurple-600))    calc(180deg + var(--shimmer-spread) * 0.84),
    /* Trough notch centred on 270° (left vertical midpoint). */
    var(--active-shimmer-trough, var(--sys-bluepurple-600))    calc(180deg + var(--shimmer-spread)),
    var(--active-shimmer-trough, var(--sys-bluepurple-600))    calc(360deg - var(--shimmer-spread)),
    /* ── Comet A left shoulder (fading back IN to wrap-around peak) ── */
    var(--active-shimmer-step-1, var(--sys-bluepurple-600))    calc(360deg - var(--shimmer-spread) * 0.84),
    var(--active-shimmer-step-3, var(--sys-bluepurple-500))    calc(360deg - var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-5, var(--sys-bluepurple-400))    calc(360deg - var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-7, var(--sys-bluepurple-400))    calc(360deg - var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-9, var(--sys-bluepurple-300))    calc(360deg - var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(360deg - var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(360deg - var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   360deg
  );

  /* Two phase-locked animations on the host's timeline.  Driven from
     the host so the inherited registered properties (--shimmer-angle,
     --shimmer-spread) cascade to BOTH pseudo layers automatically. */
  animation:
    shimmer-rotate  var(--shimmer-duration) linear      infinite,
    shimmer-breathe var(--shimmer-duration) ease-in-out infinite;
}

/* ── Sharp gradient ring (foreground) ──────────────────────────────
   `::before` paints the 1 px conic-gradient ring clipped to the
   padding box via `mask-composite: exclude`.  Reads the shared
   `--shimmer-gradient` so it stays in lockstep with the glow below. */
.active-shimmer::before {
  content: "";
  position: absolute;
  /* Mid-tier stroke alignment: extend the pseudo OUTWARD by one
     thickness so the pseudo's border-box coincides with the host's
     border-box edge (not the padding-box edge).  Combined with the
     `padding: var(--shimmer-thickness)` below and the mask-composite
     ring, the visible 1 px stroke now paints exactly ON the host's
     border edge — same pixel layer as `--container-border` /
     `border-color` colorization — instead of 1 px inset.  See
     Foundations / Keylines & Strokes for the tier system. */
  inset: calc(-1 * var(--shimmer-thickness));
  border-radius: inherit;
  padding: var(--shimmer-thickness);
  pointer-events: none;
  z-index: 1;

  background: var(--shimmer-gradient);

  /* Clip the gradient to the padding ring only.  -webkit- prefix for
     Safari compat. */
  -webkit-mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  mask-composite: exclude;
}

/* ── Blurred glow ring (background) ────────────────────────────────
   `::after` paints the SAME conic-gradient + ring mask as `::before`,
   then softens the whole thing with `filter: blur()` so the result
   reads as an outer glow that inherits every property of the sharp
   ring — rotation, breathing, centre-out falloff, all of it — with
   ZERO duplication of the gradient definition. */
.active-shimmer::after {
  content: "";
  position: absolute;
  /* Match `::before`'s mid-tier inset so the glow's source ring sits
     on the same pixel layer as the sharp ring (then blurred outward
     for the halo effect). */
  inset: calc(-1 * var(--shimmer-thickness));
  border-radius: inherit;
  /* Same 1 px source thickness as `::before` so the entire active
     effect uses a unified 1 px base.  The blur via `filter: blur()`
     below visually softens / expands the ring outward into a halo,
     but the source material itself stays 1 px exactly. */
  padding: var(--shimmer-thickness);
  pointer-events: none;
  z-index: 0;

  background: var(--shimmer-gradient);

  -webkit-mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  mask-composite: exclude;

  filter: blur(var(--shimmer-glow-blur));
}

/* ── Entrance pulse ─────────────────────────────────────────
   `@starting-style` seeds the initial `filter` value when the
   `.active-shimmer` rule first matches an element (i.e., when the
   class is added).  The element starts at `brightness(2.4)` — a
   visibly hot indigo pulse — and transitions DOWN to the regular
   `brightness(1)` over `--shimmer-enter-duration` (700 ms default)
   via the `transition: filter` rule on `.active-shimmer` above.

   The result reads as a punchy "I'm here!" announce: the active
   selection flashes brighter than its resting state, then settles
   into the regular rotation + breathing cycle.  Pure CSS — no JS
   coordination, no extra keyframe rule. */
@starting-style {
  .active-shimmer {
    filter: brightness(2.4);
  }
}

/* ── Exit fade ─────────────────────────────────────────────
   Consumer applies `.active-shimmer--leaving` for one frame BEFORE
   removing the active state entirely.  The host's existing
   `transition: opacity` channel fades the whole composition from
   1 → 0 over `--shimmer-leave-duration` (300 ms default), then the
   consumer's `setTimeout` removes both classes to unmount the effect.

   Why a separate modifier rather than transitioning on class removal?
   CSS transitions only run while the element MATCHES the rule that
   declares them.  Removing `.active-shimmer` removes the transition
   declaration along with it, so the styles snap rather than fade.
   Keeping the class while toggling the `--leaving` modifier preserves
   the transition contract for the duration of the fade. */
.active-shimmer--leaving {
  opacity: 0;
}

/* ── Paused modifier ────────────────────────────────── */
.active-shimmer--paused {
  animation-play-state: paused;
}

/* ── Inline variant ──────────────────────────────────────────────── */
.active-shimmer--inline {
  display: inline-block;
}

/* ── Interactive hover variant ───────────────────────────────────── */
/* Apply alongside `.active-shimmer` on any wrapper whose inner
   element is clickable.  Paints a static 1 px outline in
   `--active-shimmer-trough` on `:hover` plus a soft halo, giving
   the operator a static stand-in for the active state before they
   click. */
.active-shimmer--interactive {
  transition:
    border-color var(--dur-short-2, 200ms) var(--ease-standard, ease),
    box-shadow   var(--dur-short-3, 250ms) var(--ease-standard, ease);
}

.active-shimmer--interactive:hover {
  border-color: var(--active-shimmer-trough);
  box-shadow: 0 0 12px -4px var(--active-shimmer-trough);
}

/* ── Hover-shimmer variant ────────────────────────────────────────────
   Pure-CSS hover recipe: tonal-fill + animated comet ring that arrive
   and depart together.  No JS, no class toggling — everything is in
   :hover so the effect is completely self-contained.

   Architecture:
     • Host at rest: transparent 1 px border (no layout shift on hover),
       ::before pseudo present but opacity 0.
     • On :hover: fill flips to the 12% bluepurple tonal tone, ::before
       fades to opacity 1 in lockstep via the same 100 ms transition so
       ring and fill ALWAYS travel together — no split-entrance artefacts.
     • Comet angle + spread are driven by `animation` on the :hover host.
       Both are registered `@property` values with `inherits: true`, so
       they cascade into ::before automatically — zero gradient duplication.
     • animation-delay: -2.2s pre-positions the comet at ~66° so the
       first visible event on hover is the streak rounding the corner
       (~80° for a 240 × 40 px element) — immediate drama, then orbit.

   Differences from `.active-shimmer`:
     • Active-shimmer = JS-driven persistent selection ring (applied /
       removed by the component).  Hover-shimmer = pure-CSS hover preview
       that starts and stops with the pointer.
     • No entrance brightness pulse, no exit fade class — not needed for
       a pure :hover affordance.
     • 100 ms entry/exit (snappy UI feedback) vs. 700 ms / 300 ms
       (selection announcement) on active-shimmer.

   Customisable via CSS overrides on the host element:
     --shimmer-duration  (default 12s — rotation / breathing period)
     --shimmer-thickness (default 1px — ring width, inherited by ::before)

   Usage: add class `hover-shimmer` to any element with `border-radius`.
   `position: relative` and `overflow: visible` are asserted by this rule. */

.hover-shimmer {
  position: relative;
  overflow: visible;
  border: 1px solid transparent;
  transition:
    background   100ms var(--ease-emphasized-decel, ease),
    box-shadow   100ms var(--ease-emphasized-decel, ease),
    transform    100ms var(--ease-emphasized-decel, ease),
    color        100ms var(--ease-emphasized-decel, ease);
}

.hover-shimmer::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--shimmer-thickness, 1px));
  border-radius: inherit;
  padding: var(--shimmer-thickness, 1px);
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  transition: opacity 100ms var(--ease-emphasized-decel, ease);
  background: var(--shimmer-gradient, none);
  -webkit-mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(white 0 0) content-box,
    linear-gradient(white 0 0);
  mask-composite: exclude;
}

.hover-shimmer:hover {
  transform: translateY(-1px);
  background: var(--sys-bluepurple-500-12, color-mix(in srgb, var(--sys-bluepurple-500) 12%, transparent));
  color: var(--text, inherit);

  --shimmer-thickness: 1px;
  --shimmer-duration: 12s;
  --shimmer-gradient: conic-gradient(
    from var(--shimmer-angle),
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   0deg,
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-9,  var(--sys-bluepurple-300))   calc(var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-7,  var(--sys-bluepurple-400))   calc(var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-5,  var(--sys-bluepurple-400))   calc(var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-3,  var(--sys-bluepurple-500))   calc(var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-1,  var(--sys-bluepurple-600))   calc(var(--shimmer-spread) * 0.84),
    var(--active-shimmer-trough,  var(--sys-bluepurple-600))   var(--shimmer-spread),
    var(--active-shimmer-trough,  var(--sys-bluepurple-600))   calc(180deg - var(--shimmer-spread)),
    var(--active-shimmer-step-1,  var(--sys-bluepurple-600))   calc(180deg - var(--shimmer-spread) * 0.84),
    var(--active-shimmer-step-3,  var(--sys-bluepurple-500))   calc(180deg - var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-5,  var(--sys-bluepurple-400))   calc(180deg - var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-7,  var(--sys-bluepurple-400))   calc(180deg - var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-9,  var(--sys-bluepurple-300))   calc(180deg - var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(180deg - var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(180deg - var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   180deg,
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(180deg + var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(180deg + var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-9,  var(--sys-bluepurple-300))   calc(180deg + var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-7,  var(--sys-bluepurple-400))   calc(180deg + var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-5,  var(--sys-bluepurple-400))   calc(180deg + var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-3,  var(--sys-bluepurple-500))   calc(180deg + var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-1,  var(--sys-bluepurple-600))   calc(180deg + var(--shimmer-spread) * 0.84),
    var(--active-shimmer-trough,  var(--sys-bluepurple-600))   calc(180deg + var(--shimmer-spread)),
    var(--active-shimmer-trough,  var(--sys-bluepurple-600))   calc(360deg - var(--shimmer-spread)),
    var(--active-shimmer-step-1,  var(--sys-bluepurple-600))   calc(360deg - var(--shimmer-spread) * 0.84),
    var(--active-shimmer-step-3,  var(--sys-bluepurple-500))   calc(360deg - var(--shimmer-spread) * 0.68),
    var(--active-shimmer-step-5,  var(--sys-bluepurple-400))   calc(360deg - var(--shimmer-spread) * 0.52),
    var(--active-shimmer-step-7,  var(--sys-bluepurple-400))   calc(360deg - var(--shimmer-spread) * 0.36),
    var(--active-shimmer-step-9,  var(--sys-bluepurple-300))   calc(360deg - var(--shimmer-spread) * 0.20),
    var(--active-shimmer-step-11, var(--sys-bluepurple-300))   calc(360deg - var(--shimmer-spread) * 0.12),
    var(--active-shimmer-step-15, var(--sys-bluepurple-200))   calc(360deg - var(--shimmer-spread) * 0.04),
    var(--active-shimmer-step-18, var(--sys-bluepurple-100))   360deg
  );
  animation:
    shimmer-rotate  var(--shimmer-duration) linear      infinite,
    shimmer-breathe var(--shimmer-duration) ease-in-out infinite;
  animation-delay: -2.2s, -2.2s;
}

.hover-shimmer:hover::before {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .hover-shimmer:hover {
    animation: none;
    --shimmer-angle: 0deg;
    --shimmer-spread: 88deg;
  }
}

/* ── Reduced motion ───────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .active-shimmer {
    animation: none;
    /* Freeze at the apex moment: --shimmer-angle = 0° puts Peak A
       dead-centre on the top edge and Peak B dead-centre on the
       bottom edge; --shimmer-spread = 88deg stretches both comets
       to ~98% bright share of the perimeter. */
    --shimmer-angle: 0deg;
    --shimmer-spread: 88deg;
  }
}

/* ── Theme-swap suppressor ───────────────────────────────────────── */
/* When `applyTheme()` adds `.theme-swapping` to <html>, suppress all
   transitions so chrome colours flip in-sync. */
.theme-swapping .active-shimmer,
.theme-swapping .active-shimmer--interactive {
  transition: none !important;
  animation: none !important;
}

}

/* ── ui-kit.components: src/styles/components/story-headline.css ── */
@layer ui-kit.components {
/* ── StoryHeadline ────────────────────────────────────────────────────
   Rotating cross-fade marquee for the `<StoryHeadline>` primitive.

   Layout is owned by the consumer (the component is happy inside any
   card / banner / hero strip); this sheet ships only the typography,
   per-kind glyph tinting, and the cross-fade animation.

   The cross-fade trick: the component keys its inner body div by the
   rotation index, so React mounts a fresh node on every rotation.
   The CSS `m3-story-headline__body` rule then animates that fresh
   node's opacity from 0 → 1 over `--dur-short-4`, producing a soft
   fade-in without needing a JS-driven double-buffer animation
   library.  The previous node is unmounted simultaneously, so the
   eye sees a clean fade between two distinct headlines rather than
   a glitchy in-place text swap. */

.m3-story-headline {
  /* Display block + min-height so the surrounding card chrome doesn't
     reflow when a longer headline rotates in.  Tunable per consumer
     via override on the outer class. */
  display: block;
  min-height: 28px;
  font-size: var(--fs-body-md);
  line-height: var(--lh-body-md);
  letter-spacing: var(--ls-body-md);
  color: var(--text);
}

.m3-story-headline__body {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  opacity: 0;
  animation: m3-story-headline-fade-in var(--dur-short-4) var(--ease-emphasized-decel) forwards;
}

/* Snap mode — bypass the fade entirely.  Reach for this when the
   consumer wants a hard cut between headlines (e.g. dense status
   bar).  `prefers-reduced-motion` also lands here automatically
   via the kit-wide motion gate below. */
.m3-story-headline__body--snap {
  animation: none;
  opacity: 1;
}

@keyframes m3-story-headline-fade-in {
  from {
    opacity: 0;
    transform: translateY(2px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Reduced-motion: skip the fade.  Pairs with the JS-side guard that
   freezes the rotation on `prefers-reduced-motion: reduce` so a user
   who's opted out of motion sees the first headline statically. */
@media (prefers-reduced-motion: reduce) {
  .m3-story-headline__body {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* Leading glyph — single Unicode codepoint, sized one tier down from
   the body text so it reads as a quiet eyebrow on the headline. */
.m3-story-headline__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25em;
  height: 1em;
  font-family: var(--mono);
  flex-shrink: 0;
  color: var(--text-muted);
}

.m3-story-headline__text {
  min-width: 0;
}

.m3-story-headline__link {
  flex-shrink: 0;
  color: var(--text-accent);
}

/* ── Per-kind tone tints on the leading glyph ───────────────────
   Each story kind tints its own glyph so a vertical scan across
   rotated headlines reads consistent: climbers in green, droppers
   in red, milestones in accent, etc.  The text body stays at the
   canonical `--text` colour — the glyph carries the categorical
   signal, the headline carries the message. */
.m3-story-headline__body--rank-climb   .m3-story-headline__glyph { color: var(--sys-green-400); }
.m3-story-headline__body--rank-drop    .m3-story-headline__glyph { color: var(--sys-red-400); }
.m3-story-headline__body--new-entry    .m3-story-headline__glyph { color: var(--text-accent); }
.m3-story-headline__body--record-break .m3-story-headline__glyph { color: var(--sys-orangeyellow-400); }
.m3-story-headline__body--milestone    .m3-story-headline__glyph { color: var(--text-accent); }
.m3-story-headline__body--info         .m3-story-headline__glyph { color: var(--sys-greenblue-400); }

/* ── Ticker variant ─────────────────────────────────────────
   Vertical slot-machine rotation — the outgoing headline lifts UP
   and out, the incoming headline rises IN from below.  The JS layer
   (`motion/react`'s `AnimatePresence`) drives the `y` + `opacity`
   transforms; this sheet ships only the masking container chrome.

   Why a single-row clip with a mask gradient?  A hard
   `overflow: hidden` would crop the headline at a razor-sharp
   pixel edge — readable, but feels mechanical.  The mask gradient
   feathers the top + bottom 25% of the clip box so the line
   reads as floating into and out of a soft horizon, matching the
   "news headline ticker" reach the variant is designed for.

   Mask uses `mask-image` (with `-webkit-mask-image` shim for
   Safari) instead of an outer linear-gradient overlay so the
   surrounding card chrome can carry any background / blur it
   wants without painting over the mask. */
.m3-story-headline--ticker {
  position: relative;
  /* Pin to one line of body text so the slide travels exactly one
     line-box height in each direction.  `--lh-body-md` is the kit's
     body-text line-height dimension (1.25rem); reading it directly
     (rather than computing line-height × font-size) keeps the
     ticker height in lock-step with the kit's typography retunes. */
  min-height: var(--lh-body-md);
  height: var(--lh-body-md);
  overflow: hidden;
  /* Soft top + bottom feather so the headline reads as floating
     into / out of the mask rather than popping at the clip edge.
     The middle 50% of the box is fully opaque so the active
     headline is legible without the mask fighting the text.
     Consumers that bump the clip box taller (e.g. to envelope
     NodeTag chip padding) MUST narrow the feather and add flex
     centering at the page level so the resting content doesn't
     intersect the now-larger transparent zones — see
     `frontend/src/styles/pages/network-stats.css`
     `.net-today-ticker .m3-story-headline--ticker` for the
     canonical retune pattern. */
  mask-image: linear-gradient(
    to bottom,
    transparent 0%,
    black 25%,
    black 75%,
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0%,
    black 25%,
    black 75%,
    transparent 100%
  );
}

/* Inside the ticker, the active body sits in normal flow so its
   line-box drives the container height; AnimatePresence-managed
   exiting siblings are taken out of flow so they don't push the
   surrounding chrome around mid-rotation.  motion-managed
   transforms drive the slide; we just reset the kit's default
   CSS-keyframe entry so it doesn't double-fire on top of the
   motion-driven y / opacity. */
.m3-story-headline--ticker .m3-story-headline__body {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Cancel the crossfade keyframe — motion/react owns the entrance
     animation for the ticker.  Opacity is reset to 1 so the
     motion-driven opacity tween reads from a clean baseline. */
  animation: none;
  opacity: 1;
}
.m3-story-headline--ticker .m3-story-headline__text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Reduced-motion: the JS gate already freezes the rotation timer
   on the first headline, but if the consumer changes the lineup
   mid-session AnimatePresence will still try to run an exit
   transition.  Cancel transforms here so the snap is instant. */
@media (prefers-reduced-motion: reduce) {
  .m3-story-headline--ticker .m3-story-headline__body {
    transform: none !important;
    opacity: 1 !important;
    transition: none !important;
  }
}

}

/* ── ui-kit.components: src/styles/components/featured-carousel.css ── */
@layer ui-kit.components {
/* ═══════════════════════════════════════════════════════════════════════
   featured-carousel.css — editorial slot carousel used on blog /
   landing surfaces.  Horizontal peek layout: all slides are always in
   the DOM; the track translateX is animated by a companion JS spring.

   Class prefix: .fc  (featured-carousel)
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Outer shell ── */
/* Place .fc OUTSIDE the page's content container — it spans the full
   viewport; each slide is sized to the container content width by JS. */
.fc {
  position: relative;
  /* Essential: the flex track extends well past the viewport; without
     this the page gets a horizontal scrollbar. */
  overflow: hidden;
  margin-block: 0 var(--sp-12);
  /* Gap between slides. --fc-slide-w / --fc-fade-end are set by JS. */
  --fc-gap: 4px;
}

/* Strip inner .featured margin — .fc owns block spacing */
.fc .featured {
  margin-block: 0;
}

/* Disable card hover lift inside carousel — spring handles motion */
.fc .featured:hover {
  transform: none;
  box-shadow: var(--shadow-container);
}

/* ── Clip container — hides off-screen slides + gradient-fades the peeks ── */
.fc-clip {
  overflow: hidden;
  /* CRITICAL: slides have flex-shrink:0, so the track's min-content width
     is the sum of ALL slides. Without inline-size containment that
     min-content propagates up into ancestor grid/flex tracks and inflates
     the whole page layout. contain: inline-size zeroes this element's
     intrinsic inline-size contribution. */
  contain: inline-size;
  padding-block: 8px;   /* breathing room for drop-shadow */
  margin-block: -8px;
  /* Gradient mask: fades peeking slides from transparent at the viewport
     edge to solid at the active-card boundary. --fc-fade-end is set by JS
     to the gutter width (= (viewport − slideW) / 2). */
  mask-image: linear-gradient(
    to right,
    transparent 0,
    black var(--fc-fade-end, 16px),
    black calc(100% - var(--fc-fade-end, 16px)),
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    black var(--fc-fade-end, 16px),
    black calc(100% - var(--fc-fade-end, 16px)),
    transparent 100%
  );
}

/* ── Horizontal track ── */
.fc-track {
  display: flex;
  gap: var(--fc-gap);
  will-change: transform;
  /* JS sets: transform: translateX(Xpx) */
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}

.fc-track.is-dragging {
  cursor: grabbing;
}

/* ── Individual slides — always visible; JS controls opacity/scale ── */
.fc-slide {
  flex: 0 0 var(--fc-slide-w, 80%);  /* JS sets --fc-slide-w */
  min-width: 0;
  opacity: 0.45;
  transform: scale(0.96);
  transition:
    opacity   var(--dur-medium-2) var(--ease-standard),
    transform var(--dur-medium-2) var(--ease-standard);
}

.fc-slide--active {
  opacity: 1;
  transform: scale(1);
}

.fc-slide:not(.fc-slide--active):hover {
  opacity: 0.7;
}

/* ── Navigation row: [←]  ● ● ○ ○ ○  [→] ── */
/* Nav stays at the content-grid width even though .fc is full-bleed */
.fc-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
  max-width: var(--grid-body);
  margin-inline: auto;
  padding-inline: var(--page-px);
}

/* ── Prev / next buttons ── */
.fc-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: var(--shape-full);
  border: 1px solid var(--border-subtle);
  background: var(--surface-tint);
  color: var(--text-muted);
  cursor: pointer;
  padding: 0;
  font-size: 18px;
  line-height: 1;
  transition:
    background     var(--dur-short-2) var(--ease-standard),
    color          var(--dur-short-2) var(--ease-standard),
    border-color   var(--dur-short-2) var(--ease-standard);
  appearance: none;
  -webkit-appearance: none;
}

.fc-btn:hover {
  background: var(--accent-subtle);
  /* --text-accent passes WCAG AA for hover icon; --accent-light (bluepurple-400) failed. */
  color: var(--text-accent);
  border-color: var(--border-accent);
}

.fc-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.fc-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  pointer-events: none;
}

/* ── Dot indicators ── */
.fc-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  flex: 1;
}

.fc-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--shape-full);
  background: var(--border-subtle);
  border: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
  transition:
    background  var(--dur-short-2) var(--ease-standard),
    transform   var(--dur-short-2) var(--ease-standard);
  appearance: none;
  -webkit-appearance: none;
}

.fc-dot--active {
  background: var(--accent);
  transform: scale(1.35);
}

.fc-dot:hover:not(.fc-dot--active) {
  background: var(--text-tertiary);
}

.fc-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ── Slot label (eyebrow) ── */
.fc-label {
  font-family: var(--mono);
  font-size: var(--fs-label-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-accent);
  font-weight: var(--fw-bold);
}

/* ── Responsive ── */
@media (max-width: 639px) {
  .fc-btn {
    width: 30px;
    height: 30px;
    font-size: 16px;
  }
  .fc-dot {
    width: 10px;
    height: 10px;
  }
}

}

/* ── ui-kit.responsive: src/styles/responsive.css ── */
@layer ui-kit.responsive {
/* ── M3 window-size-class @media block (kit-only after Phase 4b.3 split) ─── */

/* ── Phase 4b.3 excision: live-packets page (pkt-* part 2: compact path cell → card footer) moved to `frontend/src/styles/pages/live-packets.css`. */
/* ── Responsive — M3 window size classes ──────────────────────────────── */
/* :root defaults = Expanded tier (840–1199px).  Per-page responsive
   rules are redistributed to their owning sheets in Phase 4b.3 — see
   frontend/src/styles/{hero,footer}.css and
   frontend/src/styles/pages/{uikit,live-packets,live-map}.css for the
   per-page overrides that used to live here. */

/* ── M3 Large (≥ 1200px) ──────────────────────────────────────── */
@media (min-width: 1200px) {
  :root {
    --grid-margin: var(--sp-2);  /* 8px */
    /* Page rhythm bumps for desktop — every page wrapper (.obs-page,
       .about-page, .connect-page, .account-page, .pkt-page, .nd-page,
       admin .page) tracks these without per-class media queries.
       `--section-gap` is intentionally NOT overridden here — the
       default sp-12 (48px) carries through from `:root` so desktop
       and the medium tier share one cadence; only Compact (≤599)
       tightens it to sp-8. */
    --page-pt:      var(--sp-15);  /* 60px */
    --page-pb:      var(--sp-20);  /* 80px */
  }
}

/* ── M3 Extra-large (≥ 1600px) ───────────────────────────────────────── */
@media (min-width: 1600px) {
  :root {
    --grid-body:   1400px;
    --grid-margin: var(--sp-3);  /* 12px */
  }
}

/* ── M3 Medium (≤ 839px) ─────────────────────────────────────────────── */
@media (max-width: 839px) {
  :root {
    --grid-columns: 8;
  }
  .pane-row {
    flex-direction: column;
  }
  .pane--fixed {
    width: 100%;  /* full-width when stacked; beats base rule via source order */
  }
  /* Zero container margin for full-bleed table pages (LivePackets, Network) */
  .page-bleed {
    --grid-margin: 0px;
  }
  /* SegmentedButton — allow horizontal scroll when content overflows */
  .m3-segmented {
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .m3-segmented::-webkit-scrollbar { display: none; }
}

/* ── M3 Compact (≤ 599px) ─────────────────────────────────────── */
@media (max-width: 599px) {
  :root {
    --grid-columns: 4;
    --grid-gutter: var(--sp-4);  /* 16px */
    --grid-margin: var(--sp-4);  /* 16px */
    /* Page rhythm — tighten everything for phone viewports.  Section
       cadence sits at sp-6 so dense pages don't spread vertically while
       still leaving a clear break between sections; card padding drops
       to sp-4 so the dense data cards fit narrow columns without
       squeezing content.  --page-px tightens to sp-1 since Container's
       --grid-margin already paints the 16px rail. */
    --page-pt:      var(--sp-6);   /* 24px */
    --page-pb:      var(--sp-12);  /* 48px */
    --page-px:      var(--sp-1);   /* 4px */
    --section-gap:  var(--sp-8);   /* 32px */
    --card-pad:     var(--sp-4);   /* 16px */
    --card-pad-hero: var(--sp-5);  /* 20px */
  }
  /* Snackbar — respect safe area insets, constrain to viewport */
  .m3-snackbar {
    min-width: 0;
    width: calc(100% - var(--sp-8));
    max-width: 560px;
  }
  /* GridSection demo — prevent implicit columns from span > grid-columns */
  .layout-grid > [style*="span 6"],
  .layout-grid > [style*="span 9"] {
    grid-column: 1 / -1;
  }
  /* Chips — icon-only on compact (hide label, tighten padding) */
  .m3-chip--filter .m3-chip__label { display: none; }
  .m3-chip--filter { padding: 0 var(--sp-3); gap: 0; }
  /* Range slider — shorter handles to save vertical space */
  .m3-dual-slider {
    --_sl-handle-h: 28px;
    --_sl-handle-h-press: 28px;
    --_sl-touch: 36px;
  }
}


}

/* ── ui-kit.motion: src/styles/motion.css ── */
@layer ui-kit.motion {
/* ── Universal motion reducer (@media prefers-reduced-motion) ────────────── */

/* Universal motion-reducer (KIT-owned): forces every animation /
   transition to 0.01ms when the user has set `prefers-reduced-motion`.
   The waev-logo-specific override (keep the wave bars visible) lives
   in `components/waev-logo.css`. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

}
