/* eventrace — glass layer
 *
 * Loaded after styles.css and overrides it. Kept separate deliberately: the
 * base sheet still holds the layout and the component structure, so this file
 * can be removed with one <link> and the app returns to the flat theme intact.
 *
 * Three constraints shaped every decision here:
 *
 *   1. The CSP is `script-src 'self' https://cdn.auth0.com`. No animation
 *      library can load, so the physics live in CSS `linear()` easing rather
 *      than in Framer Motion or Motion One. That is not a compromise for the
 *      60fps target — a spring encoded in the easing function runs on the
 *      compositor with no per-frame JavaScript at all.
 *
 *   2. backdrop-filter is expensive per element, not per pixel. Glass is
 *      applied to containers — the bar, the panels, the cards, the drawer —
 *      and never to table rows, of which there can be hundreds. A blurred row
 *      in a 200-row table costs 200 backdrop passes and drops frames on a
 *      laptop GPU.
 *
 *   3. Only transform, opacity and filter are animated. Anything that moves
 *      via width/top/left/box-shadow forces layout or paint each frame.
 */

/* ── spring easing ────────────────────────────────────────────────────────
 *
 * `linear()` approximates a real spring by sampling its curve, so a value can
 * overshoot and settle the way a physical control does. Browsers without it
 * fall back to the cubic-bezier below, which lands in the same place with a
 * duller finish rather than breaking.
 */
:root {
  --ease-out:    cubic-bezier(.22, 1, .36, 1);
  --ease-inout:  cubic-bezier(.65, 0, .35, 1);
  --spring:      cubic-bezier(.34, 1.56, .64, 1);
  --spring-soft: cubic-bezier(.22, 1, .36, 1);

  --t-fast: 140ms;
  --t-mid:  260ms;
  --t-slow: 420ms;
}

@supports (transition-timing-function: linear(0, 1)) {
  :root {
    /* Underdamped: a small, quick overshoot. For buttons and toggles. */
    --spring: linear(
      0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%,
      1.017, 1.077 20.8%, 1.121, 1.149 24.4%, 1.159, 1.163 27%, 1.154,
      1.129 32.2%, 1.051 39.5%, 1.017 43.3%, 0.991, 0.977 51%, 0.975 57.1%,
      0.997 69.4%, 1.003 82.9%, 1
    );
    /* Nearly critically damped: for panels and anything large, where an
       overshoot on a big surface reads as a glitch rather than as physics. */
    --spring-soft: linear(
      0, 0.007, 0.029 2.2%, 0.118, 0.246 6.4%, 0.686 12.2%, 0.87 15.4%,
      0.98, 1.032 19.3%, 1.055, 1.06 22.4%, 1.049, 1.023 27.4%, 0.995 32.1%,
      0.982 37.7%, 0.99 46.9%, 1.002 60.9%, 1
    );
  }
}

/* ── palette ──────────────────────────────────────────────────────────────
 *
 * Light is the default because the app shipped light and the data views were
 * tuned against a white ground. Dark is a deliberate second design, not an
 * inversion: the accent lifts to stay legible on a dark ground, and the glass
 * fill flips from white-over-colour to slate-over-colour.
 */
:root,
:root[data-theme="light"] {
  --bg:            #eef1f8;
  --surface:       rgba(255, 255, 255, 0.72);
  --surface-2:     rgba(255, 255, 255, 0.52);
  --surface-3:     rgba(236, 238, 248, 0.75);
  --border:        rgba(20, 24, 60, 0.10);
  --border-strong: rgba(20, 24, 60, 0.20);

  /* Denser than the dark equivalent on purpose. Light glass sits over a bright
     ground with the ambient blobs behind it, and at 0.62 the colour came
     through the fill and pulled body text down toward the blob's hue. Text
     needs a settled ground more than the panel needs transparency. */
  --glass-fill:    rgba(255, 255, 255, 0.78);
  --glass-fill-2:  rgba(255, 255, 255, 0.58);
  --glass-edge: linear-gradient(145deg,
                  rgba(255, 255, 255, 0.95) 0%,
                  rgba(255, 255, 255, 0.25) 38%,
                  rgba(124, 58, 237, 0.16) 72%,
                  rgba(46, 155, 240, 0.22) 100%);
  --glass-sheen: linear-gradient(105deg,
                  transparent 32%,
                  rgba(255, 255, 255, 0.62) 48%,
                  transparent 62%);

  --shadow:    0 1px 2px rgba(18, 22, 48, .05),
               0 8px 24px -8px rgba(18, 22, 48, .14);
  --shadow-lg: 0 2px 6px rgba(18, 22, 48, .06),
               0 24px 60px -14px rgba(18, 22, 48, .28);
  --glow: 0 0 0 1px rgba(75, 59, 224, .18), 0 6px 24px -6px rgba(75, 59, 224, .42);

  /* Ambient light sources behind the page. Weaker in light mode: on a bright
     ground the same alpha reads as haze over the whole interface rather than
     as light behind it. */
  --blob-1: rgba(124, 58, 237, 0.20);
  --blob-2: rgba(46, 155, 240, 0.18);
  --blob-3: rgba(236, 72, 153, 0.11);
  --blob-alpha: 1;

  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg:            #070912;
  --surface:       rgba(15, 23, 42, 0.62);
  --surface-2:     rgba(23, 31, 53, 0.55);
  --surface-3:     rgba(30, 39, 66, 0.62);
  --border:        rgba(255, 255, 255, 0.09);
  --border-strong: rgba(255, 255, 255, 0.20);

  --ink:           #eef1fb;
  --ink-2:         #a8b0cc;
  --ink-3:         #767e9c;

  --accent:        #8b7cff;
  --accent-deep:   #a99cff;
  --accent-weak:   rgba(139, 124, 255, 0.16);
  --accent-line:   rgba(139, 124, 255, 0.42);

  --tech: #8b7cff;
  --biz:  #f0a44a;
  --warn: #f0a44a;
  --ok:   #4ad6a4;
  --good: #4ad6a4;
  --crit: #ff7a7a;

  --s1: rgba(139, 124, 255, 0.14);
  --s2: rgba(139, 124, 255, 0.30);
  --s3: rgba(139, 124, 255, 0.50);
  --s4: rgba(139, 124, 255, 0.72);
  --s5: #8b7cff;

  --glass-fill:    rgba(15, 23, 42, 0.58);
  --glass-fill-2:  rgba(15, 23, 42, 0.38);
  --glass-edge: linear-gradient(145deg,
                  rgba(255, 255, 255, 0.28) 0%,
                  rgba(255, 255, 255, 0.06) 40%,
                  rgba(139, 124, 255, 0.24) 74%,
                  rgba(56, 189, 248, 0.30) 100%);
  --glass-sheen: linear-gradient(105deg,
                  transparent 32%,
                  rgba(255, 255, 255, 0.16) 48%,
                  transparent 62%);

  --shadow:    0 1px 2px rgba(0, 0, 0, .5),
               0 10px 30px -10px rgba(0, 0, 0, .7);
  --shadow-lg: 0 2px 8px rgba(0, 0, 0, .55),
               0 30px 70px -16px rgba(0, 0, 0, .85);
  --glow: 0 0 0 1px rgba(139, 124, 255, .34), 0 8px 30px -6px rgba(139, 124, 255, .5);

  --blob-1: rgba(109, 74, 255, 0.42);
  --blob-2: rgba(14, 165, 233, 0.34);
  --blob-3: rgba(217, 70, 239, 0.26);

  color-scheme: dark;
}

/* Someone whose OS says dark and who has not chosen otherwise gets dark. The
   guard means an explicit light choice still wins over the OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #070912;
    --surface:   rgba(15, 23, 42, 0.62);
    --surface-2: rgba(23, 31, 53, 0.55);
    --surface-3: rgba(30, 39, 66, 0.62);
    --border:        rgba(255, 255, 255, 0.09);
    --border-strong: rgba(255, 255, 255, 0.20);
    --ink: #eef1fb; --ink-2: #a8b0cc; --ink-3: #767e9c;
    --accent: #8b7cff; --accent-deep: #a99cff;
    --accent-weak: rgba(139, 124, 255, 0.16);
    --accent-line: rgba(139, 124, 255, 0.42);
    --tech: #8b7cff; --biz: #f0a44a; --warn: #f0a44a;
    --good: #4ad6a4; --crit: #ff7a7a; --ok: #4ad6a4;
    --s1: rgba(139,124,255,.14); --s2: rgba(139,124,255,.30);
    --s3: rgba(139,124,255,.50); --s4: rgba(139,124,255,.72); --s5: #8b7cff;
    --glass-fill:   rgba(15, 23, 42, 0.58);
    --glass-fill-2: rgba(15, 23, 42, 0.38);
    --glass-edge: linear-gradient(145deg,
                    rgba(255,255,255,.28) 0%, rgba(255,255,255,.06) 40%,
                    rgba(139,124,255,.24) 74%, rgba(56,189,248,.30) 100%);
    --glass-sheen: linear-gradient(105deg, transparent 32%,
                    rgba(255,255,255,.16) 48%, transparent 62%);
    --shadow:    0 1px 2px rgba(0,0,0,.5), 0 10px 30px -10px rgba(0,0,0,.7);
    --shadow-lg: 0 2px 8px rgba(0,0,0,.55), 0 30px 70px -16px rgba(0,0,0,.85);
    --glow: 0 0 0 1px rgba(139,124,255,.34), 0 8px 30px -6px rgba(139,124,255,.5);
    --blob-1: rgba(109, 74, 255, 0.42);
    --blob-2: rgba(14, 165, 233, 0.34);
    --blob-3: rgba(217, 70, 239, 0.26);
    color-scheme: dark;
  }
}

:root { --r: 14px; --r-lg: 20px; --r-sm: 10px; }

/* ── ambient light ────────────────────────────────────────────────────────
 *
 * Three blobs on a fixed layer behind everything. They drift on their own and
 * lean toward the pointer via --mx/--my, which motion.js writes at most once
 * per frame. Both are transforms on a composited layer, so the drift costs no
 * layout and no paint.
 */
body {
  background: var(--bg);
  transition: background-color var(--t-slow) var(--ease-out);
}

body::before {
  content: '';
  position: fixed;
  /* Big enough that the drift never exposes an edge, no bigger. Each vmax
     here is real GPU memory on a promoted layer. */
  inset: -18vmax;
  z-index: -2;
  pointer-events: none;
  opacity: var(--blob-alpha, 1);
  background:
    radial-gradient(38vmax 38vmax at calc(18% + var(--mx, 0px) * 1.4) calc(12% + var(--my, 0px) * 1.4),
      var(--blob-1), transparent 62%),
    radial-gradient(34vmax 34vmax at calc(82% - var(--mx, 0px) * 1.1) calc(24% + var(--my, 0px) * 0.7),
      var(--blob-2), transparent 60%),
    radial-gradient(30vmax 30vmax at calc(56% + var(--mx, 0px) * 0.6) calc(88% - var(--my, 0px) * 0.9),
      var(--blob-3), transparent 62%);
  /* No blur filter here on purpose. This layer is roughly 2000x1600px once the
     inset is applied, and blurring it re-filters every one of those pixels on
     each animated frame — measurably the most expensive thing on the page on
     integrated graphics. The radial gradients already fade to transparent at
     60%, so the blur was smoothing something that was already smooth.
     Transform-only animation keeps this on the compositor. */
  animation: drift 34s var(--ease-inout) infinite alternate;
  transition: opacity var(--t-slow) var(--ease-out);
  will-change: transform;
}

/* A fine grid, so the glass has something to refract rather than sitting on
   flat colour. Low contrast on purpose — it should register as texture. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--border) 1px, transparent 1px),
    linear-gradient(to bottom, var(--border) 1px, transparent 1px);
  background-size: 56px 56px;
  mask-image: radial-gradient(80% 60% at 50% 0%, #000 0%, transparent 78%);
  opacity: .5;
}

@keyframes drift {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  50%  { transform: translate3d(2.5%, -2%, 0) scale(1.06); }
  100% { transform: translate3d(-2%, 2.5%, 0) scale(1.02); }
}

/* ── the glass recipe ─────────────────────────────────────────────────────
 *
 * The 1px gradient border is a masked pseudo-element rather than border-image:
 * border-image cannot follow border-radius, and a gradient background clipped
 * to the padding box would sit under the backdrop-filter instead of on the
 * edge. This composites the ring cleanly at any radius.
 */
.topbar, .panel, .card, .spkcard, .acard, .drawer, .kpi, .filters, .toolbar,
.notice, .note, .empty, .tblwrap, .atablewrap {
  background: var(--glass-fill);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 0;
  border-radius: var(--r);
  box-shadow: var(--shadow);
}

/* The gradient ring is absolutely positioned, so it needs a positioned
   ancestor — but `position` must be added only where the base sheet leaves the
   element static. Setting it blanket-fashion replaced `fixed` on .drawer and
   `sticky` on .filters: the drawer stopped being a panel and rendered inline
   in the document flow, which made every card click look like it did nothing.
   A shared rule may only introduce a property, never overwrite one the base
   sheet is relying on. */
.panel, .card, .spkcard, .acard, .kpi, .toolbar,
.notice, .note, .empty, .tblwrap, .atablewrap {
  position: relative;
}

/* Already positioned in styles.css — left exactly as they are. Sticky and
   fixed both establish a containing block, so ::before anchors correctly. */
.topbar { position: sticky; }

/* The one gap: below 900px the base sheet drops .filters to static, which
   would leave its ring anchored to an ancestor instead of the panel. */
@media (max-width: 900px) {
  .filters { position: relative; }
}

.topbar::before, .panel::before, .card::before, .spkcard::before, .acard::before,
.drawer::before, .kpi::before, .filters::before, .toolbar::before,
.notice::before, .note::before, .empty::before,
.tblwrap::before, .atablewrap::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: var(--glass-edge);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
  z-index: 0;
}

.panel, .acard, .filters, .drawer { border-radius: var(--r-lg); }

/* ── chrome ───────────────────────────────────────────────────────────────*/
.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  border-radius: 0;
  backdrop-filter: blur(22px) saturate(200%);
  -webkit-backdrop-filter: blur(22px) saturate(200%);
  background: var(--glass-fill-2);
  box-shadow: 0 1px 0 var(--border), 0 10px 30px -22px rgba(10, 14, 40, .5);
}
.topbar::before { padding: 0 0 1px; background: var(--glass-edge); }

.wordmark { letter-spacing: -0.015em; }
.mark { filter: drop-shadow(0 2px 8px rgba(124, 58, 237, .34)); }

/* ── controls ─────────────────────────────────────────────────────────────
 *
 * Every interactive surface gets the same three-part response: a lift on
 * hover, a compression on press, and a sheen that sweeps once on hover. The
 * lift and the compression are transforms; the sheen is an opacity-and-
 * transform pseudo-element. Nothing here touches layout.
 */
.btn, .navbtn, .modebtn, .authbtn, .iconbtn, .sortbtn, .linkbtn, .select, .alink {
  border-radius: var(--r-sm);
  transition:
    transform var(--t-fast) var(--spring),
    box-shadow var(--t-mid) var(--ease-out),
    background-color var(--t-mid) var(--ease-out),
    color var(--t-mid) var(--ease-out),
    border-color var(--t-mid) var(--ease-out);
}

.btn, .navbtn, .modebtn, .authbtn, .iconbtn {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* Sheen. Parked off to the left, swept across on hover. */
.btn::after, .navbtn::after, .modebtn::after, .authbtn::after, .iconbtn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--glass-sheen);
  transform: translate3d(-120%, 0, 0);
  opacity: 0;
  pointer-events: none;
  z-index: 1;
}

.btn:hover::after, .navbtn:hover::after, .modebtn:hover::after,
.authbtn:hover::after, .iconbtn:hover::after {
  opacity: 1;
  transform: translate3d(120%, 0, 0);
  transition: transform 620ms var(--ease-out), opacity 620ms var(--ease-out);
}

.btn:hover, .authbtn:hover, .iconbtn:hover, .navbtn:hover, .modebtn:hover {
  transform: translate3d(0, -1px, 0);
  box-shadow: var(--shadow);
}

.btn:active, .authbtn:active, .iconbtn:active,
.navbtn:active, .modebtn:active, .sortbtn:active {
  transform: scale(.98) translate3d(0, 0, 0);
  transition-duration: 60ms;
}

.btn:focus-visible, .navbtn:focus-visible, .modebtn:focus-visible,
.authbtn:focus-visible, .iconbtn:focus-visible, .select:focus-visible,
.linkbtn:focus-visible, .alink:focus-visible, .sortbtn:focus-visible,
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.navbtn.is-active, .modebtn.is-active {
  background: var(--accent-weak);
  color: var(--accent-deep);
  box-shadow: inset 0 0 0 1px var(--accent-line), 0 4px 14px -6px var(--accent-line);
}

/* Magnetic pull. motion.js writes --px/--py from the pointer's offset inside
   the element; the element leans toward it and springs back on leave. */
[data-magnetic] {
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0);
  transition: transform 220ms var(--spring);
}
[data-magnetic]:active { transform: translate3d(var(--px, 0px), var(--py, 0px), 0) scale(.98); }

/* Click ripple, positioned by motion.js at the pointer. */
.ripple {
  position: absolute;
  border-radius: 50%;
  transform: translate3d(-50%, -50%, 0) scale(0);
  background: radial-gradient(circle, currentColor 0%, transparent 70%);
  opacity: .32;
  pointer-events: none;
  z-index: 2;
  animation: ripple 620ms var(--ease-out) forwards;
}
@keyframes ripple {
  to { transform: translate3d(-50%, -50%, 0) scale(1); opacity: 0; }
}

/* ── inputs ───────────────────────────────────────────────────────────────*/
.searchbox, .select, .atext, .afield input, .afield select, input[type="search"], input[type="text"] {
  border-radius: var(--r-sm);
  background: var(--glass-fill-2);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--border);
  transition: border-color var(--t-mid) var(--ease-out),
              box-shadow var(--t-mid) var(--ease-out),
              background-color var(--t-mid) var(--ease-out);
}
.searchbox:focus-within, .select:focus, .atext:focus,
.afield input:focus, .afield select:focus {
  border-color: var(--accent-line);
  box-shadow: var(--glow);
  background: var(--surface);
}

/* ── cards ────────────────────────────────────────────────────────────────*/
.card, .spkcard {
  transition:
    transform var(--t-mid) var(--spring-soft),
    box-shadow var(--t-mid) var(--ease-out);
  transform-style: preserve-3d;
}
.card:hover, .spkcard:hover {
  transform: translate3d(0, -3px, 0);
  box-shadow: var(--shadow-lg);
}

/* Tilt. motion.js writes the two angles; the card keeps its hover lift by
   composing both in one transform. */
[data-tilt] {
  transform:
    perspective(900px)
    rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg))
    translate3d(0, var(--lift, 0px), 0);
  transition: transform var(--t-mid) var(--spring-soft);
}
[data-tilt]:hover { --lift: -3px; }

/* A specular highlight that tracks the pointer across the card. */
[data-tilt]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    22rem 22rem at var(--gx, 50%) var(--gy, 0%),
    rgba(255, 255, 255, .16), transparent 62%);
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease-out);
  pointer-events: none;
  z-index: 1;
}
[data-tilt]:hover::after { opacity: 1; }

/* ── status, pills, tags ──────────────────────────────────────────────────*/
.pill, .tag, .rowtag, .tierbadge, .status {
  border-radius: 999px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.pill-ok   { box-shadow: 0 0 0 1px color-mix(in srgb, var(--good) 34%, transparent),
                         0 0 14px -3px color-mix(in srgb, var(--good) 60%, transparent); }
.pill-wait { box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 34%, transparent),
                         0 0 14px -3px color-mix(in srgb, var(--warn) 60%, transparent); }

.st-confirmed { box-shadow: 0 0 10px -2px color-mix(in srgb, var(--good) 70%, transparent); }
.st-expected  { box-shadow: 0 0 10px -2px color-mix(in srgb, var(--warn) 70%, transparent); }

/* ── data surfaces ────────────────────────────────────────────────────────
 *
 * Rows are NOT glass. A blurred backdrop per row multiplies the cost by the
 * row count; the container carries the effect and the rows stay cheap.
 */
.tblwrap, .atablewrap { overflow: auto hidden; }
.atable tbody tr, .tblwrap tbody tr {
  transition: background-color var(--t-fast) var(--ease-out);
}
.atable tbody tr:hover, .tblwrap tbody tr:hover { background: var(--surface-2); }

.meter, .budgetbar, .score-bar, .track, .ramp {
  border-radius: 999px;
  overflow: hidden;
}
.meter > *, .budgetbar > *, .score-bar > *, .track > * {
  border-radius: 999px;
  transition: transform var(--t-slow) var(--spring-soft);
}

/* ── view transitions ─────────────────────────────────────────────────────
 *
 * Views already toggle via .is-active. This adds the entrance without
 * touching the switching logic in app.js.
 */
.view { display: none; }
.view.is-active {
  display: block;
  animation: viewIn var(--t-slow) var(--spring-soft) both;
}
@keyframes viewIn {
  from { opacity: 0; transform: translate3d(0, 10px, 0) scale(.995); }
  to   { opacity: 1; transform: none; }
}

/* Cards stagger in. --i is set by motion.js; capped so a 200-card list does
   not take twelve seconds to finish arriving. */
.cards > *, .spk-grid > *, .kpis > * {
  animation: cardIn 420ms var(--spring-soft) both;
  animation-delay: calc(var(--i, 0) * 26ms);
}
@keyframes cardIn {
  from { opacity: 0; transform: translate3d(0, 12px, 0) scale(.98); }
  to   { opacity: 1; transform: none; }
}

/* ── drawer & scrim ───────────────────────────────────────────────────────*/
.scrim {
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  transition: opacity var(--t-mid) var(--ease-out);
}
/* No entrance animation here. The base sheet slides the drawer with
   `transform: translateX(100%)` toggled by .is-open, and an animation with
   fill-mode `both` outranks a regular declaration in the cascade — it pinned
   `transform: none` the moment it finished on page load, so the drawer sat
   open-positioned behind `visibility: hidden` and never slid. Upgrading the
   existing transition is the correct place to add spring feel. */
.drawer { box-shadow: var(--shadow-lg); }

@media (prefers-reduced-motion: no-preference) {
  .drawer {
    transition: transform var(--t-slow) var(--spring-soft), visibility var(--t-slow);
  }
}

/* ── skeletons ────────────────────────────────────────────────────────────*/
.skeleton {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--glass-sheen);
  transform: translate3d(-100%, 0, 0);
  animation: shimmer 1.4s var(--ease-inout) infinite;
}
@keyframes shimmer { to { transform: translate3d(100%, 0, 0); } }

/* ── toasts ───────────────────────────────────────────────────────────────*/
.toaster {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
}
.toast {
  position: relative;
  min-width: 220px;
  max-width: 380px;
  padding: 11px 14px;
  border-radius: var(--r);
  background: var(--glass-fill);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  box-shadow: var(--shadow-lg);
  color: var(--ink);
  font-size: 13px;
  pointer-events: auto;
  animation: toastIn 320ms var(--spring) both;
}
.toast::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: var(--glass-edge);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
}
.toast.is-out { animation: toastOut 220ms var(--ease-out) both; }
.toast-good { box-shadow: var(--shadow-lg), 0 0 0 1px color-mix(in srgb, var(--good) 40%, transparent); }
.toast-bad  { box-shadow: var(--shadow-lg), 0 0 0 1px color-mix(in srgb, var(--crit) 40%, transparent); }
@keyframes toastIn  { from { opacity: 0; transform: translate3d(0, 14px, 0) scale(.96); } }
@keyframes toastOut { to   { opacity: 0; transform: translate3d(0, 8px, 0) scale(.98); } }

/* ── command palette ──────────────────────────────────────────────────────*/
.cmdk {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: start center;
  padding-top: 14vh;
  /* A modal has to read as a layer above the page, not as a tint over it. At
     50% the interface behind stayed legible enough to compete with the
     palette's own text. */
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  animation: fadeIn 180ms var(--ease-out) both;
}
.cmdk[hidden] { display: none; }
.cmdk-panel {
  position: relative;
  width: min(560px, calc(100vw - 32px));
  border-radius: var(--r-lg);
  /* Denser than a panel: this one sits over arbitrary content. */
  background: color-mix(in srgb, var(--bg) 62%, var(--glass-fill));
  backdrop-filter: blur(24px) saturate(190%);
  -webkit-backdrop-filter: blur(24px) saturate(190%);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: cmdkIn 300ms var(--spring-soft) both;
}
.cmdk-panel::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: var(--glass-edge);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
}
.cmdk-input {
  width: 100%;
  padding: 16px 18px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--ink);
  font-size: 15px;
  outline: none;
}
.cmdk-list { max-height: 46vh; overflow-y: auto; padding: 6px; margin: 0; list-style: none; }
.cmdk-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--r-sm);
  cursor: pointer;
  color: var(--ink);
  transition: background-color 120ms var(--ease-out), transform 120ms var(--spring);
}
.cmdk-item:hover, .cmdk-item.is-sel { background: var(--accent-weak); transform: translate3d(2px, 0, 0); }
.cmdk-item .k { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.cmdk-empty { padding: 18px; color: var(--ink-3); font-size: 13px; text-align: center; }
@keyframes fadeIn { from { opacity: 0; } }
@keyframes cmdkIn { from { opacity: 0; transform: translate3d(0, -12px, 0) scale(.97); } }

.cmdk-hint {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 7px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--glass-fill-2);
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 11px;
}

/* ── theme toggle ─────────────────────────────────────────────────────────*/
.themebtn {
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--glass-fill-2);
  color: var(--ink-2);
  cursor: pointer;
  transition: transform var(--t-mid) var(--spring),
              color var(--t-mid) var(--ease-out),
              box-shadow var(--t-mid) var(--ease-out);
}
.themebtn:hover { color: var(--accent); box-shadow: var(--glow); transform: rotate(18deg); }
.themebtn:active { transform: scale(.94); }
.themebtn svg { width: 15px; height: 15px; }

/* ── drag & drop ──────────────────────────────────────────────────────────*/
[draggable="true"] { cursor: grab; }
[draggable="true"]:active { cursor: grabbing; }
.is-dragging {
  opacity: .5;
  transform: scale(.98) rotate(.6deg);
  transition: transform var(--t-fast) var(--ease-out);
}
.drop-target {
  outline: 2px dashed var(--accent-line);
  outline-offset: 3px;
  background: var(--accent-weak);
  transition: background-color var(--t-fast) var(--ease-out);
}

/* ── accessibility and cost ───────────────────────────────────────────────
 *
 * Three escape hatches, in order of how much they remove.
 */

/* Motion off: everything still works, nothing moves. The blobs stop rather
   than continuing to animate behind a user who asked for stillness. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  body::before { animation: none; }
  [data-tilt], [data-magnetic] { transform: none !important; }
}

/* Transparency off: keep the layout and the depth, drop the blur. This is a
   real accessibility setting, not a preference — blur behind text is what
   some readers cannot parse. */
@media (prefers-reduced-transparency: reduce) {
  .topbar, .panel, .card, .spkcard, .acard, .drawer, .kpi, .filters, .toolbar,
  .notice, .note, .empty, .tblwrap, .atablewrap, .toast, .cmdk-panel, .cmdk {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--bg);
  }
  body::before { opacity: .25; }
}

/* Forced colours: hand the whole thing over to the user's palette. */
@media (forced-colors: active) {
  .topbar, .panel, .card, .spkcard, .acard, .drawer, .kpi, .filters, .toast, .cmdk-panel {
    border: 1px solid CanvasText;
    background: Canvas;
    backdrop-filter: none;
  }
  body::before, body::after { display: none; }
}

/* ── print ───────────────────────────────────────────────────────────────
 *
 * There is no PDF library here. The CSP admits no third-party script, and
 * self-hosting one would be a few hundred kilobytes to lay out a table the
 * printer already lays out correctly. "Download PDF" adds two class names and
 * calls window.print(); everything that makes the result a document rather
 * than a screenshot of an app is below.
 *
 * These rules live at the end of this file, not in styles.css, because they
 * have to win against the glass layer above — the translucent panel
 * backgrounds and blurs are declared here, and a print rule of equal
 * specificity in the earlier stylesheet would lose to them.
 */
@media print {
  /* Landscape: the table is ten columns wide and portrait breaks it. */
  @page { size: landscape; margin: 12mm; }

  html, body {
    background: #fff !important;
    color: #101014 !important;
  }
  /* The ambient washes are two full-bleed pseudo-elements. On paper they are
     a solid page of ink. */
  body::before, body::after { display: none !important; }

  .topbar, .mainnav, .foot, .scrim, .drawer, .filters, .searchrow,
  .cmdk, .cmdk-panel, .toast, .btn, .linkbtn,
  /* The note it carries is already in the meta line below the heading. */
  .panel-tools { display: none !important; }

  /* Only the panel that asked to be printed, and only its own section. */
  body.is-printing .viewhead,
  body.is-printing .stack > *:not(.print-target) { display: none !important; }

  .print-target {
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    backdrop-filter: none !important;
    padding: 0 !important;
  }

  /* Ink on paper, whatever the screen was showing.
     This is the rule that matters: the interface is dark by default, so
     everything inside it carries a light foreground and a dark surface. Left
     alone, a printed table is white text on white paper with a black band
     where the header should be — which is exactly what the first version of
     this stylesheet produced. Everything below re-states the few colours that
     still mean something on paper. */
  .print-target, .print-target * {
    color: #101014 !important;
    background: transparent !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }
  .print-target .print-meta,
  .print-target .locmore,
  .print-target thead th { color: #55596b !important; }
  .print-only { display: block !important; }
  .print-meta {
    font-family: var(--mono);
    font-size: 8.5pt;
    letter-spacing: .04em;
    color: #55596b;
    margin: 0 0 10px;
  }
  .panel-head { margin-bottom: 4px; }
  .panel-head h2 { font-size: 13pt; }

  /* A wide table on screen scrolls sideways; on paper it has to fit. */
  .tblwrap { overflow: visible !important; }
  /* The roster is a fixed-height scroller on screen. On paper it is however
     many pages it takes, and a sticky header inside a table-header-group is
     two mechanisms for the same job fighting each other. */
  .scroller { max-height: none !important; overflow: visible !important; }
  .scroller thead th { position: static !important; box-shadow: none !important; }
  /* Sort carets and the row-count hint are screen affordances. */
  .sortbtn .car { display: none !important; }
  table { width: 100%; border-collapse: collapse; font-size: 8.5pt; }
  th, td { padding: 4px 6px !important; }
  /* Repeat the column headers on every page, and never split a row across
     two of them. */
  thead { display: table-header-group; }
  tr { break-inside: avoid; page-break-inside: avoid; }
  th { border-bottom: 1px solid #101014 !important; }
  td { border-bottom: 1px solid #d8dae4 !important; }

  /* Bars are data here, not decoration, so they have to survive the printer's
     default of dropping backgrounds. */
  .sharebar, .sharebar i {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
  .print-target .sharebar { background: #e6e7ef !important; }
  .print-target .sharebar i { background: #4f46e5 !important; }

  a[href]::after { content: ''; }   /* no URL footnotes on a data table */
}
