/* =============================================================================
   BASE — reset, typography, layout primitives, utilities.
   Reads tokens.css only. No colour literals below this line except pure
   black/white alpha used for scrims.
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; border-color: var(--border); }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background .35s ease, color .35s ease;
  overflow: hidden;               /* the shell owns scrolling, not the page */
}

/* Ambient background wash. Fixed, behind everything, pointer-transparent. */
.bgfx {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: var(--bg-fx);
  transition: background .5s ease;
}

img, svg, video { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
table { border-collapse: collapse; width: 100%; }

h1, h2, h3, h4, h5 { letter-spacing: -.014em; font-weight: 700; line-height: 1.25; }
h1 { font-size: 24px; }
h2 { font-size: 18px; }
h3 { font-size: 15px; }
h4 { font-size: 13px; }

::selection { background: color-mix(in srgb, var(--primary) 26%, transparent); }

/* One focus ring for the whole product. Never removed — only restyled. */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 4px;
}

/* -----------------------------------------------------------------------------
   SCROLLBARS — thin, token-coloured, always present in dense panels.
   -------------------------------------------------------------------------- */
.scroll { overflow: auto; scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.scroll::-webkit-scrollbar-track { background: transparent; }
.scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
.scroll::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }

/* -----------------------------------------------------------------------------
   TYPE UTILITIES
   -------------------------------------------------------------------------- */
.t-xs   { font-size: 11px; }
.t-sm   { font-size: 12px; }
.t-base { font-size: 13px; }
.t-md   { font-size: 15px; }
.t-lg   { font-size: 18px; }
.t-xl   { font-size: 22px; }
.t-2xl  { font-size: 28px; }
.t-3xl  { font-size: 36px; }
.t-4xl  { font-size: 46px; }

.w-4 { font-weight: 400; } .w-5 { font-weight: 500; }
.w-6 { font-weight: 600; } .w-7 { font-weight: 700; } .w-8 { font-weight: 800; }

.c-muted { color: var(--muted); }
.c-faint { color: var(--faint); }
.c-text  { color: var(--text); }
.c-pri   { color: var(--on-link); }
.c-acc   { color: var(--accent); }
/* Status text carries real figures — a budget overrun, a days-late count — at
   11-12px, where 4.5:1 is required. The raw --success/--warning/--error tokens
   are chosen for brand fit and several land near 3.5:1 on their own background.
   Mixing toward --text keeps the hue legible as green/amber/red while getting
   the contrast, and does the right thing in both light and dark themes without
   a fork (--text is dark on one and light on the other). */
.c-ok    { color: var(--on-ok); }
.c-warn  { color: var(--on-warn); }
.c-err   { color: var(--on-err); }

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.num  { font-variant-numeric: tabular-nums; letter-spacing: -.01em; }

/* A label above a number. Used on every KPI in the product, so it is defined
   once rather than re-specified per card. */
.eyebrow {
  font-size: 10px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}

.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.nowrap { white-space: nowrap; }

/* -----------------------------------------------------------------------------
   LAYOUT PRIMITIVES
   -------------------------------------------------------------------------- */
.row  { display: flex; align-items: center; }
.col  { display: flex; flex-direction: column; }
.between { justify-content: space-between; }
.center  { justify-content: center; align-items: center; }
.end     { justify-content: flex-end; }
.wrap    { flex-wrap: wrap; }
.grow    { flex: 1 1 auto; min-width: 0; }
.none    { flex: none; }
.start   { align-items: flex-start; }
.stretch { align-items: stretch; }
.baseline { align-items: baseline; }

.g-2 { gap: 2px; } .g-4 { gap: 4px; } .g-6 { gap: 6px; } .g-8 { gap: 8px; }
.g-10 { gap: 10px; } .g-12 { gap: 12px; } .g-14 { gap: 14px; } .g-16 { gap: 16px; }
.g-20 { gap: 20px; } .g-24 { gap: 24px; } .g-32 { gap: 32px; }

.grid { display: grid; gap: 12px; }
/* A grid column declared as bare `1fr` is really `minmax(auto, 1fr)`, and
   `auto` refuses to shrink below its content. That is why a three-panel row
   overflowed the page at 1100px instead of squeezing. Every grid child opts
   out of that here, once. */
.grid > * { min-width: 0; }
.gc-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gc-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.gc-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.gc-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.gc-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.span-2 { grid-column: span 2; } .span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; } .span-full { grid-column: 1 / -1; }

.hide { display: none !important; }
/* Flex and grid children default to min-width:auto, which stops `truncate`
   from ever kicking in inside a row. This is the opt-out. */
.min-w-0 { min-width: 0; }
.rel { position: relative; }

/* Spacing — a dashboard-density scale (8/12/16/20/24), not a marketing one. */
.mt-4 { margin-top: 4px; } .mt-6 { margin-top: 6px; } .mt-8 { margin-top: 8px; }
.mt-12 { margin-top: 12px; } .mt-16 { margin-top: 16px; } .mt-20 { margin-top: 20px; }
.mt-24 { margin-top: 24px; }
.mb-4 { margin-bottom: 4px; } .mb-8 { margin-bottom: 8px; }
.mb-12 { margin-bottom: 12px; } .mb-16 { margin-bottom: 16px; } .mb-20 { margin-bottom: 20px; }
.p-8 { padding: 8px; } .p-12 { padding: 12px; } .p-14 { padding: 14px; }
.p-16 { padding: 16px; } .p-20 { padding: 20px; } .p-24 { padding: 24px; }

/* -----------------------------------------------------------------------------
   RESPONSIVE COLLAPSE

   The dense layouts are designed for a 1440px+ control-room screen. Below that
   they have to fold rather than scroll sideways — a dashboard the reader has to
   pan horizontally is a broken dashboard.

   `!important` is deliberate and is the correct tool here: several screens set
   `grid-template-columns` inline (a 1.4fr/1fr split reads better next to the
   markup it lays out than a class name would), and an inline style cannot
   otherwise be overridden by a media query.

   Breakpoints are expressed in viewport width, but what actually matters is the
   page width, which is roughly viewport minus the 232px rail and 36px of
   padding. Hence the collapse starts at 1240px rather than the more obvious
   1024px.
   -------------------------------------------------------------------------- */
/* These are `[data-w~="lt-N"]` rather than `@media (max-width: N)` on purpose.

   The UI can be zoomed (see the zoom notes in store.js), and CSS `zoom` changes
   how much layout room there is without changing what a media query sees: at
   80% on a 1280px screen the design has 1600px to lay out into, but every
   media query still reads 1280. Keyed off media queries, a six-column KPI row
   would collapse to three at exactly the point zooming out had made room for
   six.

   store.js writes the effective width onto <html> as data-w tokens and these
   rules key off those, so the ladder responds to the space that actually
   exists. Device breakpoints — the rail becoming a drawer, the auth hero
   hiding — stay on real media queries elsewhere, because those ask about the
   physical screen rather than about layout room.

   All four have the same specificity (0,2,0), so source order decides and they
   stay in descending width order. */
[data-w~="lt-1400"] .gc-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Any inline multi-column split becomes a single column. */
[data-w~="lt-1240"] .grid[style*="grid-template-columns"] { grid-template-columns: minmax(0, 1fr) !important; }
[data-w~="lt-1240"] .gc-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
[data-w~="lt-1240"] .gc-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

[data-w~="lt-980"] .gc-6,
[data-w~="lt-980"] .gc-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
[data-w~="lt-980"] .gc-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
[data-w~="lt-980"] .span-2,
[data-w~="lt-980"] .span-3,
[data-w~="lt-980"] .span-4 { grid-column: auto; }

[data-w~="lt-700"] .gc-6,
[data-w~="lt-700"] .gc-5,
[data-w~="lt-700"] .gc-4,
[data-w~="lt-700"] .gc-3,
[data-w~="lt-700"] .gc-2 { grid-template-columns: minmax(0, 1fr); }

/* -----------------------------------------------------------------------------
   MOTION — 150–300ms, meaningful only. Every one of these is disabled under
   prefers-reduced-motion at the bottom of the block.
   -------------------------------------------------------------------------- */
@keyframes fade-up   { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes fade-in   { from { opacity: 0; } to { opacity: 1; } }
@keyframes slide-r   { from { opacity: 0; transform: translateX(22px); } to { opacity: 1; transform: none; } }
@keyframes slide-l   { from { opacity: 0; transform: translateX(-22px); } to { opacity: 1; transform: none; } }
@keyframes pop       { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }

/* Centred variants of the entry animations.
 *
 * A dialog centres itself with `transform: translate(-50%, -50%)`. `pop`
 * animates the same property, so for the length of the animation the centring
 * translate is simply gone — the dialog is laid out with its top-left corner at
 * the middle of the screen, then snaps into place when the animation ends and
 * the element's own transform applies again. A 200ms jump on every dialog, on
 * every open.
 *
 * A keyframe cannot inherit part of a transform, so the translate has to be
 * carried through each frame. One variant per centring axis. */
@keyframes pop-c     { from { opacity: 0; transform: translate(-50%, -50%) scale(.96); }
                       to   { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
@keyframes pop-cx    { from { opacity: 0; transform: translateX(-50%) scale(.97); }
                       to   { opacity: 1; transform: translateX(-50%) scale(1); } }
@keyframes up-cx     { from { opacity: 0; transform: translateX(-50%) translateY(10px); }
                       to   { opacity: 1; transform: translateX(-50%) translateY(0); } }
@keyframes spin      { to { transform: rotate(360deg); } }
@keyframes pulse-ring {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--error) 45%, transparent); }
  70%  { box-shadow: 0 0 0 10px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@keyframes bar-grow  { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes draw      { to { stroke-dashoffset: 0; } }
@keyframes sweep     { from { opacity: .25; } to { opacity: 1; } }

/* `backwards`, not `both`. With `both` the last keyframe stays applied, so the
   page wrapper keeps a transform — and a transformed ancestor becomes the
   containing block for `position: fixed`, which centres modals in the page
   instead of the viewport. On a long portfolio table you then have to scroll to
   find the dialog you just opened. */
.anim-up   { animation: fade-up .38s cubic-bezier(.4,0,.2,1) backwards; }
.anim-in   { animation: fade-in .22s ease backwards; }
.anim-r    { animation: slide-r .28s cubic-bezier(.4,0,.2,1) backwards; }
.anim-l    { animation: slide-l .28s cubic-bezier(.4,0,.2,1) backwards; }
.anim-pop  { animation: pop .18s cubic-bezier(.4,0,.2,1) backwards; }

/* Stagger for card grids and list rows. Capped at 10 — past that the last card
   waits half a second and the page reads as slow, not as choreographed. */
.stagger > * { animation: fade-up .38s cubic-bezier(.4,0,.2,1) backwards; }
.stagger > *:nth-child(1) { animation-delay: .02s; }
.stagger > *:nth-child(2) { animation-delay: .05s; }
.stagger > *:nth-child(3) { animation-delay: .08s; }
.stagger > *:nth-child(4) { animation-delay: .11s; }
.stagger > *:nth-child(5) { animation-delay: .14s; }
.stagger > *:nth-child(6) { animation-delay: .17s; }
.stagger > *:nth-child(7) { animation-delay: .20s; }
.stagger > *:nth-child(8) { animation-delay: .23s; }
.stagger > *:nth-child(9) { animation-delay: .26s; }
.stagger > *:nth-child(n+10) { animation-delay: .29s; }

@media (prefers-reduced-motion: reduce) {
  .anim-up, .anim-in, .anim-r, .anim-l, .anim-pop, .stagger > * {
    animation: none !important;
  }
  * { transition-duration: .01ms !important; }
}
