/* ==========================================================================
   VaultedOS — Product UI kit  (`pui-`)
   --------------------------------------------------------------------------
   Real HTML/CSS reconstructions of *the Vaulted OS app itself*, for use inside
   marketing "product shot" panels. The receipts and reports bands both render
   app chrome; these primitives exist so the two cannot drift apart.

   Rules for this file
   -------------------
   • Every class is namespaced `pui-`. Every custom property is `--pui-*`.
   • No element selectors at the top level, no resets, no @font-face.
     It is safe to load globally alongside tokens.css / base.css.
   • Sizes are em-relative to `.pui`. Set ONE font-size on the `.pui` root and
     the whole app scales — that is how a marketing panel shrinks the product
     shot without shipping a bitmap.
     Two calibrations are in play and each rule says which it uses:
       – the dashboard chrome (shell / tiles / table) was drawn @15px root;
       – the panel readout primitives (kicker / rows / confirm / meter) were
         drawn @16px root, which is what a `.pui` inside normal page copy
         inherits from `body`.
     Both are pure em, so either scales; the comments only tell you what the
     numbers were before they were divided.
   • Brand values come from tokens.css. The app-chrome palette below has no
     equivalent in tokens.css (those tokens describe the *marketing site*, not
     the product), so it is declared once here and nowhere else.
   • EVERY colour a component paints reads through a `--pui-*` property. That
     is not decoration: it is the only reason `.pui--light` can exist as a
     dozen re-pointed properties instead of a duplicate stylesheet. If you add
     a component, do not hard-code a colour inside it.
   ========================================================================== */

.pui {
  /* ---- App chrome palette (sampled from the shipped Vaulted OS UI) ------- */
  --pui-bg: #060611;            /* app canvas                                */
  --pui-surface: #0f1624;       /* cards, table body                         */
  --pui-surface-2: #0d1826;     /* raised / selected segment                 */
  --pui-surface-sunk: #080d19;  /* inset control (entity switcher)           */
  --pui-line: #152839;          /* row rules inside a --pui-surface card     */
  --pui-line-strong: #283541;   /* container outlines                        */
  --pui-stroke: rgba(255, 255, 255, .06);
  --pui-stroke-strong: rgba(255, 255, 255, .10);
  /* Hairline for a readout that sits on the BAND, not inside a card. It has
     to hold against --bg-deep, so it is lighter than --pui-line. */
  --pui-rule: rgba(255, 255, 255, .16);
  /* Opaque base a translucent accent is mixed into (color-mix has no
     backdrop, so the mix needs the surface it will sit on). */
  --pui-mix-base: #0b1220;

  --pui-text: #ebeff2;
  --pui-muted: #8b9ba6;
  --pui-dim: rgba(235, 239, 242, .45);
  /* Blue used as TEXT. Kept apart from --pui-accent-blue (a graphic fill)
     because the light theme has to swap this one and must not swap that one. */
  --pui-blue-text: var(--vos-blue);
  --pui-accent-mint: var(--vos-mint-bright);
  --pui-hairline: linear-gradient(90deg, rgba(80, 161, 249, .9), rgba(95, 252, 192, .55));

  --pui-pos: #00d492;           /* money in                                  */
  --pui-up: #00ce8e;            /* delta improving                           */
  --pui-up-bg: rgba(0, 206, 142, .14);
  --pui-down: #ff5a66;          /* delta worsening                           */
  --pui-down-bg: rgba(223, 32, 46, .18);
  /* Informational status chip. #007df3 was the sampled app value but carries
     WHITE text at 4.02:1 — under AA. Darkened one step to #0070d9: the label
     is now 4.86:1, and the pill still clears 3:1 against BOTH surfaces it
     lands on (3.72:1 on --pui-surface dark, 4.86:1 on white). Do not lighten
     it back toward the app's swatch; the app is not shipping it at 11px on a
     marketing band. */
  --pui-info: #0070d9;          /* informational status chip                 */
  --pui-alert: #881523;         /* count badge on a review queue             */
  --pui-chip-bg: rgba(255, 255, 255, .08);

  --pui-accent-blue: #016fff;
  --pui-accent-teal: #2fa3bd;
  --pui-accent-green: #01c589;
  --pui-accent-gold: #bc960a;

  /* Readout components (see "Panel readout" below). */
  --pui-meter-track: rgba(255, 255, 255, .16);
  --pui-meter-fill: var(--vos-blue);
  --pui-meter-knob: var(--vos-mint-bright);
  /* Ring in the surface colour around the knob. The knob is a dot sitting ON
     the fill, and on the light band navy-on-blue measures 1.59:1 — it simply
     disappears. A surface-coloured ring is the standard fix for an end-marker
     that overlaps its own mark, and it costs nothing on the dark band. */
  --pui-meter-ring: var(--pui-surface);
  /* Hairline around the TRACK, for a meter with nothing else drawing its
     extent. Off by default: a meter inside `.pui-statusframe` already has a
     frame telling you where 100% is, and doubling that reads as a box in a
     box. `.pui-meter--progress` turns it on, because an upload row has no
     frame. Both steps measure ~3.5:1 against their own surface, which is what
     makes the unfilled remainder legible (the track fill itself is 1.6:1 on
     BOTH bands — that is the value it has always had, and it is the reason
     this property exists rather than a darker track). */
  --pui-meter-edge: transparent;
  --pui-meter-edge-on: rgba(255, 255, 255, .35);
  --pui-confirm-bg: rgba(255, 255, 255, .01);
  --pui-confirm-bloom: rgba(95, 252, 192, .25);
  --pui-confirm-shadow: 0 .286em 1.536em rgba(0, 0, 0, .33);
  --pui-confirm-mark: var(--vos-mint-bright);

  /* ---- Chart marks (see "CHARTS" at the foot of this file) --------------- */
  /* A six-slot CATEGORICAL scale, derived and validated per the dataviz
     method — NOT the --pui-accent-* tile family, which is a four-tile
     decoration set and was never checked for series separation. Every slot is
     a selected step: the light and dark columns are the same six hues re-
     stepped for their own surface, never one column dimmed. Full derivation,
     measured separations and the series cap are documented at the foot. */
  --pui-cat-1: #4c92f2;         /* blue    — the brand anchor, slot 1 always */
  --pui-cat-2: #02ad6d;         /* green                                     */
  --pui-cat-3: #8d7fee;         /* violet                                    */
  --pui-cat-4: #d9639c;         /* magenta                                   */
  --pui-cat-5: #be8403;         /* gold                                      */
  --pui-cat-6: #0da2c6;         /* teal                                      */
  /* Chart chrome. All derived from values `.pui--light` already re-points, so
     the light column inherits them for free. */
  --pui-chart-grid: var(--pui-line);
  --pui-chart-axis: var(--pui-line-strong);
  /* The 2px separator between touching marks is the SURFACE showing through,
     never a stroke drawn around the mark. */
  --pui-mark-gap: var(--pui-surface);
  --pui-mark-track: var(--pui-meter-track);

  /* ---- Containers -------------------------------------------------------- */
  --pui-shadow: 0 .5em 2em rgba(0, 0, 0, .35);

  --pui-r: .667em;              /* 10px @15px base — the app's card radius   */
  --pui-r-sm: .533em;
  --pui-pad: 1.667em;           /* 25px @15px base — app gutter              */

  position: relative;
  font-family: var(--font);
  font-weight: var(--fw-medium);
  line-height: 1.4;
  color: var(--pui-text);
  background: var(--pui-bg);
  /* Money must never jitter between rows. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

.pui *,
.pui *::before,
.pui *::after { box-sizing: border-box; }

/* --------------------------------------------------------------------------
   `.pui--light` — the same kit on a white marketing band
   --------------------------------------------------------------------------
   `.pui` hard-set a dark canvas, which meant the statements band (flat white)
   could not use the kit at all and had to hand-build its own copy of the
   readout. This variant re-points custom properties ONLY — there is not one
   duplicated component rule below, and there must never be one. If a light
   band needs a component to look different, add a property to `.pui` and
   re-point it here.

   Contrast: every value below is >= 4.5:1 as TEXT on --lt-panel (#ffffff),
   or >= 3:1 where it is a non-text graphic. The blue is --vos-blue-ink, never
   --vos-blue: --vos-blue is 2.7:1 on white and must never carry text.
   -------------------------------------------------------------------------- */

.pui--light {
  --pui-bg: var(--lt-panel);
  --pui-surface: var(--lt-panel);
  --pui-surface-2: #f2f6fb;
  --pui-surface-sunk: #f7f9fc;
  --pui-line: var(--lt-rule-navy);
  --pui-line-strong: var(--lt-stroke);
  --pui-stroke: var(--lt-stroke);
  --pui-stroke-strong: rgba(11, 18, 32, .16);
  --pui-rule: var(--lt-rule-navy);
  --pui-mix-base: #ffffff;

  --pui-text: var(--lt-text);          /* 17.4:1 */
  --pui-muted: var(--lt-text-muted);   /*  6.0:1 */
  /* NOT --lt-text-dim: that is 3.0:1 and fails as text. A "dim" line on a
     light band is still a sentence someone has to read. */
  --pui-dim: var(--lt-text-muted);
  --pui-blue-text: var(--vos-blue-ink);/*  6.6:1 */
  /* --vos-mint-bright is 1.3:1 on white. The greeting is a heading, so it
     takes the darkened success green instead. */
  --pui-accent-mint: color-mix(in srgb, var(--vos-success) 55%, #000);
  --pui-hairline: linear-gradient(90deg, var(--vos-blue-ink), var(--vos-success));

  --pui-pos: color-mix(in srgb, var(--vos-success) 55%, #000);
  --pui-up: color-mix(in srgb, var(--vos-success) 52%, #000);
  --pui-up-bg: rgba(0, 212, 146, .16);
  --pui-down: color-mix(in srgb, var(--vos-danger) 66%, #000);
  --pui-down-bg: rgba(255, 69, 69, .13);
  --pui-chip-bg: rgba(11, 18, 32, .07);

  /* Graphic fills, not text — 3:1 is the bar and all four clear it on white. */
  --pui-accent-blue: var(--vos-blue-ink);
  --pui-accent-teal: #1c7c93;
  --pui-accent-green: #017a55;
  --pui-accent-gold: #8a6e07;

  /* Six categorical slots, same six hues re-stepped for a white surface at
     OKLCH L ≈ .545 (teal .570, where sRGB stops having chroma to give). Every
     one clears 3:1 on #ffffff — 4.3:1 at worst. */
  --pui-cat-1: #286ecb;
  --pui-cat-2: #068553;
  --pui-cat-3: #6d5cc7;
  --pui-cat-4: #b23f7a;
  --pui-cat-5: #966802;
  --pui-cat-6: #0784a3;

  --pui-shadow: 0 .25em 2.2em rgba(11, 18, 32, .10);

  --pui-meter-track: var(--lt-track);
  /* NOT --vos-blue: 1.67:1 against --lt-track, so the filled portion and the
     empty portion were the same bar. --vos-blue-ink measures 4.08:1 against
     the track and 6.56:1 against the white card. */
  --pui-meter-fill: var(--vos-blue-ink);
  --pui-meter-knob: var(--vos-navy-deep);
  --pui-meter-edge-on: rgba(11, 18, 32, .5);
  --pui-confirm-bg: var(--lt-glass);
  --pui-confirm-bloom: rgba(0, 212, 146, .30);
  --pui-confirm-shadow: 0 .286em 1.536em rgba(11, 18, 32, .12);
  --pui-confirm-mark: color-mix(in srgb, var(--vos-success) 58%, #000);
}

/* No app canvas: the readout sits directly on whatever band is behind it.
   Use when the panel is loose UI on a section background rather than a card —
   an opaque `--pui-bg` there would punch a rectangle out of the band's glows. */
.pui--bare { --pui-bg: transparent; }

/* --------------------------------------------------------------------------
   App shell
   -------------------------------------------------------------------------- */

.pui-app {
  display: flex;
  flex-direction: column;
  gap: 1.35em;
  padding: 1.4em var(--pui-pad) var(--pui-pad);
}

.pui-appbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1em;
}

/* The green "Good evening, <name>" line. Uses the brand's bright mint. */
.pui-greeting {
  margin: 0;
  font-size: 2.133em;          /* 32px @15px base */
  font-weight: var(--fw-semibold);
  line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--pui-accent-mint);
}

.pui-datestamp {
  margin: .1em 0 0;
  font-size: 1em;
  font-weight: var(--fw-regular);
  line-height: 1.3;
  color: var(--pui-muted);
}

/* Uppercase tracked group label ("FINANCIAL PULSE"). */
.pui-grouplabel {
  margin: 0;
  font-size: .8em;             /* 12px */
  font-weight: var(--fw-semibold);
  letter-spacing: .12em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--pui-muted);
}

/* Same label, preceded by the blue→mint hairline used above a work queue. */
.pui-grouplabel--ruled {
  display: flex;
  align-items: center;
  gap: 1em;
}
.pui-grouplabel--ruled::before {
  content: '';
  flex: none;
  width: 2.4em;
  height: 1px;
  background: var(--pui-hairline);
}

.pui-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1em;
}

/* --------------------------------------------------------------------------
   Controls — entity switcher + period segmented control
   -------------------------------------------------------------------------- */

.pui-control {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  padding: .5em .75em;
  border: 1px solid var(--pui-stroke);
  border-radius: var(--pui-r-sm);
  background: var(--pui-surface-sunk);
  font-size: .867em;           /* 13px */
  font-weight: var(--fw-medium);
  line-height: 1;
  color: var(--pui-text);
  white-space: nowrap;
}
.pui-control__icon { flex: none; width: 1.15em; height: 1.15em; }
.pui-control__caret { flex: none; width: 1em; height: 1em; opacity: .6; }

.pui-seg {
  display: flex;
  align-items: center;
  gap: .15em;
  padding: .25em;
  border: 1px solid var(--pui-stroke);
  border-radius: var(--pui-r-sm);
  background: var(--pui-surface-sunk);
  font-size: .867em;           /* 13px */
  line-height: 1;
}
.pui-seg__item {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  padding: .5em .75em;
  border: 1px solid transparent;
  border-radius: .38em;
  background: transparent;
  font-weight: var(--fw-medium);
  color: var(--pui-muted);
  white-space: nowrap;
  cursor: default;
}
.pui-seg__item svg { flex: none; width: 1.1em; height: 1.1em; }
.pui-seg__item.is-active {
  border-color: var(--pui-stroke-strong);
  background: var(--pui-surface-2);
  color: var(--pui-text);
}

/* --------------------------------------------------------------------------
   Tiles — the KPI row
   -------------------------------------------------------------------------- */

.pui-tiles {
  display: grid;
  grid-template-columns: repeat(var(--pui-tile-cols, 4), minmax(0, 1fr));
  gap: .8em;
}

.pui-tile {
  --pui-tile-accent: var(--pui-accent-blue);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .5em;
  overflow: hidden;
  padding: .75em .75em .85em;
  border: 1px solid var(--pui-stroke);
  border-radius: var(--pui-r);
  background: var(--pui-surface);
}
/* Light catching the top edge, tinted by the tile's own metric family. */
.pui-tile::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent 0%, var(--pui-tile-accent) 38%, transparent 100%);
  opacity: .85;
  pointer-events: none;
}
.pui-tile::after {
  content: '';
  position: absolute;
  inset: -60% 40% auto -10%;
  height: 120%;
  background: radial-gradient(60% 60% at 40% 0%, var(--pui-tile-accent), transparent 70%);
  opacity: .10;
  pointer-events: none;
}

.pui-tile--blue  { --pui-tile-accent: var(--pui-accent-blue); }
.pui-tile--teal  { --pui-tile-accent: var(--pui-accent-teal); }
.pui-tile--green { --pui-tile-accent: var(--pui-accent-green); }
.pui-tile--gold  { --pui-tile-accent: var(--pui-accent-gold); }

.pui-tile__head {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: .6em;
}

.pui-tile__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.85em;
  height: 1.85em;
  border-radius: var(--pui-r-sm);
  background: rgba(255, 255, 255, .06);
  background: color-mix(in srgb, var(--pui-tile-accent) 22%, var(--pui-mix-base));
  color: var(--pui-tile-accent);
}
.pui-tile__icon svg { width: 1.15em; height: 1.15em; }

/* Expander affordance ("> 4 accounts") on a drill-down tile. */
.pui-tile__more {
  position: relative;
  display: flex;
  align-items: center;
  gap: .45em;
  margin: 0;
  font-size: .8em;
  font-weight: var(--fw-regular);
  line-height: 1;
  color: var(--pui-muted);
}
.pui-tile__more svg { flex: none; width: 1em; height: 1em; }

/* --------------------------------------------------------------------------
   Metric — value + label pair. Works inside a tile or standalone.
   -------------------------------------------------------------------------- */

.pui-metric { min-width: 0; }

.pui-metric__value {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .25em .4em;
  margin: 0;
  font-size: 1.3em;            /* Figma is 22/15; trimmed so the value and its
                                  delta stay on one line once a marketing
                                  panel shrinks the app below full size. */
  font-weight: var(--fw-semibold);
  line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--pui-text);
}

.pui-metric__label {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5em;
  margin: .25em 0 0;
  font-size: .8em;             /* 12px */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-muted);
}

.pui-metric__note {
  margin: .2em 0 0;
  font-size: .8em;
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-dim);
}

/* --------------------------------------------------------------------------
   Delta — the trend pill beside a metric
   -------------------------------------------------------------------------- */

.pui-delta {
  display: inline-flex;
  align-items: center;
  gap: .15em;
  padding: .2em .45em;
  border-radius: var(--r-pill);
  font-size: .55em;            /* ~11px relative to the metric value */
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  white-space: nowrap;
}
.pui-delta svg { flex: none; width: 1em; height: 1em; }

.pui-delta--up   { color: var(--pui-up);   background: var(--pui-up-bg); }
.pui-delta--down { color: var(--pui-down); background: var(--pui-down-bg); }

/* --------------------------------------------------------------------------
   Chips — status labels and counts
   -------------------------------------------------------------------------- */

.pui-chip {
  display: inline-flex;
  align-items: center;
  gap: .35em;
  padding: .2em .6em;
  border-radius: var(--r-pill);
  font-size: .917em;           /* 11px when set on a 12px label */
  font-weight: var(--fw-semibold);
  line-height: 1.45;
  white-space: nowrap;
  color: var(--pui-text);
  background: var(--pui-chip-bg);
}

.pui-chip--ok    { color: #fff; background: var(--pui-info); }
.pui-chip--good  { color: #04150f; background: var(--pui-pos); }
.pui-chip--warn  { color: #1b1403; background: #e8b33a; }
.pui-chip--alert { color: #fff; background: var(--pui-alert); }

/* --------------------------------------------------------------------------
   Table — the review queue
   -------------------------------------------------------------------------- */

.pui-tablewrap {
  border: 1px solid var(--pui-line-strong);
  border-radius: var(--pui-r);
  background: var(--pui-surface);
  overflow: hidden;
}

.pui-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 1em;
  text-align: left;
}

.pui-table caption { text-align: left; }

.pui-table thead th {
  padding: .75em 1em;
  border-bottom: 1px solid var(--pui-line);
  font-size: 1em;
  font-weight: var(--fw-medium);
  line-height: 1.4;
  color: var(--pui-text);
  white-space: nowrap;
}

.pui-table tbody th,
.pui-table tbody td {
  padding: .6em 1em;
  border-bottom: 1px solid var(--pui-line);
  font-size: 1em;
  font-weight: var(--fw-regular);
  line-height: 1.4;
  color: var(--pui-text);
}

.pui-table tbody tr:last-child > * { border-bottom: 0; }

/* Row header — the date a row is identified by. */
.pui-table tbody th[scope="row"] {
  font-size: .867em;           /* 13px */
  font-weight: var(--fw-regular);
  color: var(--pui-muted);
  white-space: nowrap;
}

.pui-table .pui-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pui-amount--pos { color: var(--pui-pos); }
.pui-amount--neg { color: var(--pui-text); }

.pui-table__foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5em;
  padding: .85em 1em;
  border-top: 1px solid var(--pui-line);
  font-size: .933em;
  font-weight: var(--fw-regular);
  line-height: 1.3;
  color: var(--pui-muted);
}
.pui-table__foot svg { flex: none; width: 1em; height: 1em; }

/* ==========================================================================
   PANEL READOUT
   --------------------------------------------------------------------------
   A "readout" is the small standalone Vaulted panel a marketing band puts
   beside its copy — a kicker, a short list of label/value pairs, and one
   summary affordance (a confirmation pill or a progress meter). It is NOT the
   dashboard: no chrome, no toolbar, no grid.

   Two bands render one today (statements' "Vaulted Report", receipts'
   "Receipt details") and they are the same object drawn on two backgrounds,
   so they share these four primitives and differ only by `.pui--light`.

   All four are calibrated @16px `.pui` root — the size a `.pui` inherits when
   it is dropped into ordinary page copy. Every derived em carries its px
   target in a comment, because several of them are ems of a 14px child rather
   than of the root and the arithmetic is not guessable.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Panel kicker — the blue label above a readout's data
   --------------------------------------------------------------------------
   API:  .pui-panelkicker              title-case ("Vaulted Report")
         .pui-panelkicker--upper       all-caps  ("RECEIPT DETAILS")
   Use a real heading element (h3/h4): it names the panel for a screen reader.
   -------------------------------------------------------------------------- */

.pui-panelkicker {
  margin: 0 0 2.143em;          /* 30px — 2.143 × this element's own 14px    */
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1;
  color: var(--pui-blue-text);
}

.pui-panelkicker--upper { text-transform: uppercase; }

/* --------------------------------------------------------------------------
   Rows — the single-record label/value readout
   --------------------------------------------------------------------------
   This is deliberately NOT `.pui-table`. `.pui-table` is a <thead>+<tbody>
   data grid: many records, one shape, column headers. A readout is ONE record
   whose fields each have their own name — that is a description list, so the
   element is <dl> and the primitive is separate. Do not merge them.

   Markup:
     <dl class="pui-rows">
       <div class="pui-rows__row">
         <dt>Cash Position</dt>
         <dd>$428,829</dd>
       </div>
       …
     </dl>

   Modifiers:
     .pui-rows--icons    each <dt> leads with a 24px <svg>; the icon, not the
                         label, sets the row height (the pitch stays 53px)
     .pui-rows--strong   values render SemiBold instead of Regular

   Knobs:
     --pui-rows-pitch    baseline-to-baseline block pitch. Default 53px.
     --pui-rows-pad      label → rule. Default 14px.
     --pui-rows-body     height of the tallest thing in a row. Set by the
                         --icons modifier; you should not need to touch it.

   The gap after each rule is COMPUTED from those three, which is why a row
   with an icon and a row without one still land on the same pitch without
   anybody hand-matching two numbers in two sections.

   <dd> wraps. It is `margin-left:auto` + `min-width:0` +
   `overflow-wrap:break-word` on purpose: a nine-figure balance or a long
   merchant name has to break inside the card, never push out through its
   right edge. Do not add `white-space:nowrap`.
   -------------------------------------------------------------------------- */

.pui-rows {
  --pui-rows-pitch: 3.3125em;   /* 53px — the Figma block pitch              */
  --pui-rows-pad: .875em;       /* 14px — content → rule                     */
  --pui-rows-body: 1.00625em;   /* 16.1px — a 14px label's own line box      */
  margin: 0;
}

.pui-rows--icons  { --pui-rows-body: 1.5em; }   /* 24px icon governs instead */
.pui-rows--strong { --pui-rows-value-weight: var(--fw-semibold); }

.pui-rows__row {
  display: flex;
  align-items: center;
  gap: 1em;                     /* 16px minimum label → value separation     */
  padding-bottom: var(--pui-rows-pad);
  border-bottom: 1px solid var(--pui-rule);
}

/* pitch − content − pad − rule. 53−16.1−14−1 = 21.9 without icons;
   53−24−14−1 = 14 with them. Same pitch, one implementation. */
.pui-rows__row + .pui-rows__row {
  margin-top: calc(var(--pui-rows-pitch) - var(--pui-rows-body) - var(--pui-rows-pad) - 1px);
}

.pui-rows__row dt {
  display: flex;
  align-items: center;
  gap: 1.143em;                 /* 16px — 1.143 × the 14px label             */
  min-width: 0;
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.15;
  color: var(--pui-muted);
}

/* The optional leading-icon slot. 24px = 1.714 × the 14px label it sits in. */
.pui-rows__row dt > svg,
.pui-rows__icon {
  flex: none;
  width: 1.714em;
  height: 1.714em;
  color: currentcolor;
}

.pui-rows__row dd {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: .143em .429em;           /* 2px / 6px on a 14px value                 */
  margin: 0 0 0 auto;           /* right-align to the rule, not to a guess   */
  min-width: 0;
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--pui-rows-value-weight, var(--fw-regular));
  line-height: 1.15;
  text-align: right;
  color: var(--pui-text);
  overflow-wrap: break-word;
}

/* --------------------------------------------------------------------------
   Inline delta — a trend figure printed beside a value, with no pill
   --------------------------------------------------------------------------
   `.pui-delta` (above) is a filled pill sized to sit next to a 22px KPI
   figure; at a 14px readout value it renders ~8px. This modifier is the same
   component at readout scale: no background, blue, 10px.
   -------------------------------------------------------------------------- */

.pui-delta--inline {
  gap: .1em;
  padding: 0;
  font-size: .714em;            /* 10px on a 14px value                      */
  background: none;
  color: var(--pui-blue-text);
}
.pui-delta--inline svg { width: 1.2em; height: 1.2em; }  /* 12px */

/* --------------------------------------------------------------------------
   Confirm — the wide "Matched & Secured." confirmation pill
   --------------------------------------------------------------------------
   API:  <p class="pui-confirm"><svg …/> Matched &amp; Secured.</p>

   Figma draws a fixed 204×45 box. It is expressed here as a MINIMUM with
   `width: max-content`, never a fixed width: the string is copy, and a longer
   one ("Matched & Secured · 3 line items") has to grow the pill rather than
   clip inside it. `max-width:100%` keeps it inside a narrow panel.

   Placement is the caller's: this primitive sets no margin. A band that wants
   it flushed right adds `margin-left:auto` itself.
   -------------------------------------------------------------------------- */

.pui-confirm {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .571em;                  /* 8px on a 14px label                       */
  width: max-content;
  min-width: 14.571em;          /* 204px                                     */
  max-width: 100%;
  min-height: 3.214em;          /* 45px                                      */
  margin: 0;
  padding: .714em 1em;          /* 10px 14px                                 */
  border-radius: 1em;           /* 14px                                      */
  background: var(--pui-confirm-bg);
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.25;
  text-align: center;
  color: var(--pui-text);
  /* The mint bloom is the whole affordance — it is what reads as "secured". */
  box-shadow: var(--pui-confirm-shadow), inset 0 0 1.05em var(--pui-confirm-bloom);
}

.pui-confirm svg {
  flex: none;
  width: 1.786em;               /* 25px                                      */
  height: 1.786em;
  color: var(--pui-confirm-mark);
}

/* --------------------------------------------------------------------------
   Meter — the "On Track" progress read-out
   --------------------------------------------------------------------------
   ACCESSIBILITY — ship this markup, not a variation of it:

     <p id="x-status">On Track</p>
     <div class="pui-meter" role="progressbar"
          aria-labelledby="x-status"
          aria-valuemin="0" aria-valuemax="100" aria-valuenow="80"
          aria-valuetext="80% of plan"
          style="--pui-meter-value:80%">
       <span class="pui-meter__fill"></span>
       <span class="pui-meter__knob"></span>
     </div>

   Both halves of that are load-bearing, and both have already been got wrong
   once in this codebase:

     • `role="progressbar"` + `aria-valuenow` + `aria-labelledby`, ALWAYS.
       An earlier build shipped it `aria-hidden="true"`, which silently threw
       the number away — the whole point of the component is the number.
     • `progressbar`, never `slider`. An earlier build used slider, and the
       knob at the fill terminus then promised a drag that does nothing. This
       is a read-out. It has no interaction contract and must not imply one.

   The knob is decorative reinforcement of the terminus, not a handle.

   One knob drives both halves: set `--pui-meter-value` (a percentage) on the
   `.pui-meter` element and keep `aria-valuenow` in step with it.
   -------------------------------------------------------------------------- */

.pui-meter {
  position: relative;
  height: .375em;               /* 6px                                       */
  border-radius: .1875em;       /* 3px                                       */
  background: var(--pui-meter-track);
  box-shadow: inset 0 0 0 1px var(--pui-meter-edge);
}

.pui-meter__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--pui-meter-value, 0%);
  border-radius: inherit;
  background: var(--pui-meter-fill);
}

.pui-meter__knob {
  position: absolute;
  top: 50%;
  left: var(--pui-meter-value, 0%);
  width: .6875em;               /* 11px                                      */
  height: .6875em;
  margin: -.34375em 0 0 -.34375em;
  border-radius: 50%;
  background: var(--pui-meter-knob);
  /* Reads against the fill it sits on AND the track it sits beside. Note it
     paints ~2px OUTSIDE the knob box, so a meter pinned hard against its
     container at 0% or 100% wants ~1em of inset — `.pui-statusframe` does
     this for you. */
  box-shadow: 0 0 0 .125em var(--pui-meter-ring);
}

/* --------------------------------------------------------------------------
   `.pui-meter--progress` — the same read-out, doing a running operation
   --------------------------------------------------------------------------
   An upload is a genuine progressbar with a MOVING value, so all of the ARIA
   above still applies verbatim — role, aria-valuenow, aria-labelledby, and
   `aria-valuetext="Uploading, 62%"` so the number is spoken as what it means.
   Keep aria-valuenow in step with --pui-meter-value; a live region is the
   caller's business, not the component's.

   Markup:
     <div class="pui-meter pui-meter--progress" role="progressbar"
          aria-labelledby="f1-name" aria-valuemin="0" aria-valuemax="100"
          aria-valuenow="62" aria-valuetext="Uploading, 62%"
          style="--pui-meter-value:62%"><span class="pui-meter__fill"></span></div>

   Three differences from the read-out flavour, all deliberate:
     • NO knob. The knob marks a terminus you are meant to read; on a moving
       bar it reads as a handle you are meant to drag.
     • The track hairline is ON (see --pui-meter-edge). A file row has no
       frame around the bar, so nothing else says where 100% is.
     • Add `.is-done` when the operation finishes: the fill turns the
       money-in green, which is the app's own "this landed" colour. Pair it
       with a real "Uploaded" chip — colour never carries the state alone.
   -------------------------------------------------------------------------- */

.pui-meter--progress {
  height: .3125em;              /* 5px — quieter than the headline read-out  */
  --pui-meter-edge: var(--pui-meter-edge-on);
}
.pui-meter--progress.is-done { --pui-meter-fill: var(--pui-pos); }

/* ==========================================================================
   CONTAINERS
   --------------------------------------------------------------------------
   Two of them, because the site draws two different objects and collapsing
   them is how the kit would start growing modifiers:

     .pui-panel   the marketing READOUT's own box — floats on a band, soft
                  24px corners, a shadow, no border. This is what statements
                  hand-built as `.st-report`.
     .pui-card    an IN-APP card — 10px corners, a hairline border, no shadow
                  (cards inside an app do not float), and a header row.

   Both are calibrated @16px `.pui` root. Both may be set on the SAME element
   as `.pui` itself (`class="pui pui--light pui-panel"`) — neither uses a
   parent selector.
   ========================================================================== */

/* --------------------------------------------------------------------------
   .pui-panel
   --------------------------------------------------------------------------
   Markup:  <div class="pui pui--light pui-panel"> …readout… </div>
   ARIA:    none of its own. It is a box. Whatever names the panel is inside
            it — normally a `.pui-panelkicker` on a real h3/h4.
   Knobs:   --pui-panel-pad (default 20px) · --pui-panel-r (default 24px)
   Trap:    it paints --pui-surface, which on `.pui--bare` is still the DARK
            app surface. `--bare` only clears the app CANVAS. A panel that
            wants the band showing through sets --pui-panel-bg: transparent.
   -------------------------------------------------------------------------- */

.pui-panel {
  --pui-panel-pad: 1.25em;      /* 20px                                      */
  --pui-panel-r: 1.5em;         /* 24px                                      */
  --pui-panel-bg: var(--pui-surface);
  position: relative;
  padding: var(--pui-panel-pad);
  border-radius: var(--pui-panel-r);
  background: var(--pui-panel-bg);
  box-shadow: var(--pui-shadow);
}

/* --------------------------------------------------------------------------
   .pui-card
   --------------------------------------------------------------------------
   Markup:
     <section class="pui-card" aria-labelledby="cash-t">
       <div class="pui-card__head">
         <h4 class="pui-card__title" id="cash-t">Cash on hand</h4>
         <p class="pui-card__aside">Updated 6:02am</p>
       </div>
       <div class="pui-card__body"> … </div>
     </section>

   ARIA:    give the card a real sectioning element plus `aria-labelledby`
            pointing at its own __title, or a screen-reader user gets a pile
            of unrelated numbers with no group boundary.
   Modifier:
     .pui-card--flush   body has no padding — for a `.pui-table` that must
                        run edge to edge (the table draws its own gutters).
   Trap:    __aside is NOT a control slot. If the app really shows a button
            there, it is a `.pui-btn` still life; see that component.
   -------------------------------------------------------------------------- */

.pui-card {
  --pui-card-pad: .9375em;      /* 15px                                      */
  display: flex;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--pui-line-strong);
  border-radius: var(--pui-r);
  background: var(--pui-surface);
}

.pui-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .35em .75em;
  padding: var(--pui-card-pad);
  border-bottom: 1px solid var(--pui-line);
}

.pui-card__title {
  margin: 0;
  min-width: 0;
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.25;
  color: var(--pui-text);
}

.pui-card__aside {
  display: flex;
  align-items: center;
  gap: .5em;
  margin: 0 0 0 auto;
  font-size: .75em;             /* 12px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-muted);
}

.pui-card__body { padding: var(--pui-card-pad); }
.pui-card--flush > .pui-card__body { padding: 0; }

/* --------------------------------------------------------------------------
   .pui-statusframe — the bordered box that packages a meter
   --------------------------------------------------------------------------
   Markup:
     <div class="pui-statusframe">
       <p class="pui-statusframe__label" id="plan-status">
         <svg …aria-hidden="true"/> On Track
       </p>
       <div class="pui-meter" role="progressbar" aria-labelledby="plan-status"
            aria-valuemin="0" aria-valuemax="100" aria-valuenow="80"
            aria-valuetext="80% of plan" style="--pui-meter-value:80%">
         <span class="pui-meter__fill"></span>
         <span class="pui-meter__knob"></span>
       </div>
     </div>

   ARIA:    the __label carries the id the meter points at with
            aria-labelledby. Without it the meter announces a bare number.
   Trap:    the frame INSETS the meter by 18px each side on purpose — the
            knob paints ~7px past the fill terminus, and at 0% or 100% a
            tighter inset clips it against the frame.
   Border:  70% of --pui-blue-text over the surface = 3.9:1 dark / 3.4:1
            light. The 50% the statements band hand-rolled measured 2.3:1 on
            white, which is under the 3:1 non-text minimum for a boundary
            that is the only thing saying where the meter ends.
   -------------------------------------------------------------------------- */

.pui-statusframe {
  padding: .375em 0 .5em;       /* 6px / 8px                                 */
  min-height: 2.8125em;         /* 45px                                      */
  border: 1px solid color-mix(in srgb, var(--pui-blue-text) 70%, transparent);
  border-radius: .75em;         /* 12px                                      */
}

.pui-statusframe__label {
  display: flex;
  align-items: center;
  gap: .5em;                    /* 6px on a 12px label                       */
  margin: 0 1.16667em;          /* 14px                                      */
  font-size: .75em;             /* 12px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-text);
}
.pui-statusframe__label svg {
  flex: none;
  width: 1.16667em;             /* 14px                                      */
  height: 1.16667em;
  color: var(--pui-blue-text);
}

.pui-statusframe .pui-meter { margin: .5625em 1.125em 0; }   /* 9px / 18px   */

/* ==========================================================================
   STILL-LIFE BUTTON
   --------------------------------------------------------------------------
   .pui-btn is a <span>. That is the whole point of it, and it is not an
   oversight to be tidied up later.

   The reports band already shipped this bug once: a marketing "product shot"
   is a picture of an app, and every <button> inside one is a real tab stop
   that leads nowhere. Worse, these shots are deliberately clipped by their
   panel, so the keyboard user gets dropped into a fake product that is half
   off-screen and cannot be scrolled to. The fix was to strip every control
   out of the dashboard, and this primitive exists so nobody re-introduces
   them one section at a time.

   REQUIRED markup — all three parts:
     <span class="pui-btn pui-btn--primary" aria-hidden="true">Upload receipt</span>

     • <span>, never <button>/<a>/<input>  → no tab stop, no role
     • aria-hidden="true"                  → the label reads as an actionable
       affordance and nothing happens; say what it MEANT in the figcaption
     • no tabindex, ever

   If a marketing section genuinely needs a working action, that is a
   `.vos-btn` from base.css sitting OUTSIDE the product shot. Do not upgrade
   this one.
   ========================================================================== */

.pui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  min-height: 2.25em;           /* 36px                                      */
  padding: .5em 1em;            /* 8px 16px                                  */
  border: 1px solid var(--pui-stroke-strong);
  border-radius: .75em;         /* 12px — matches --r-md                     */
  background: var(--pui-chip-bg);
  font-size: .8125em;           /* 13px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.25;
  white-space: nowrap;
  color: var(--pui-text);
  /* Not a cursor:pointer. It does not point anywhere. */
  cursor: default;
  user-select: none;
}
.pui-btn svg { flex: none; width: 1.15em; height: 1.15em; }

/* Blue-hairline primary — the same shape the real app and `.vos-btn--primary`
   use. Label measures 5.9:1 dark / 5.5:1 light on its own fill. */
.pui-btn--primary {
  border-color: var(--pui-blue-text);
  background: color-mix(in srgb, var(--pui-blue-text) 12%, var(--pui-mix-base));
  color: var(--pui-blue-text);
}

.pui-btn--ghost { background: transparent; }

.pui-btn--sm {
  min-height: 1.875em;          /* 30px                                      */
  padding: .35em .75em;
  font-size: .75em;             /* 12px                                      */
}

/* ==========================================================================
   RECEIPTS UPLOAD
   ========================================================================== */

/* --------------------------------------------------------------------------
   .pui-dropzone — the drag-and-drop target, at rest
   --------------------------------------------------------------------------
   Markup:
     <div class="pui-dropzone" aria-hidden="true">
       <svg class="pui-dropzone__icon" …/>
       <p class="pui-dropzone__title">Drop receipts here</p>
       <p class="pui-dropzone__hint">PDF, JPG or HEIC · up to 20 at a time</p>
     </div>

   ARIA:    aria-hidden, same reasoning as `.pui-btn` — this is a picture of a
            file input. NEVER put a real <input type="file"> inside it: it is
            focusable, it opens a system dialog, and it is sitting in a
            marketing page. What the zone SAYS ("PDF, JPG or HEIC") belongs in
            the section's own figcaption so a screen reader still gets it.
   Modifier:
     .is-dragover   the mid-drag moment, if the section wants to show it.
   Border:  70% of --pui-blue-text over the surface = 3.9:1 dark / 3.4:1
            light, so the dashed outline reads as a boundary on both bands.
   -------------------------------------------------------------------------- */

.pui-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .5em;
  padding: 2em 1.25em;
  border: 2px dashed color-mix(in srgb, var(--pui-blue-text) 70%, transparent);
  border-radius: 1em;           /* 16px                                      */
  background: color-mix(in srgb, var(--pui-blue-text) 6%, var(--pui-mix-base));
  text-align: center;
}

.pui-dropzone.is-dragover {
  border-style: solid;
  border-color: var(--pui-blue-text);
  background: color-mix(in srgb, var(--pui-blue-text) 14%, var(--pui-mix-base));
}

.pui-dropzone__icon {
  flex: none;
  width: 2.25em;                /* 36px                                      */
  height: 2.25em;
  color: var(--pui-blue-text);
}

.pui-dropzone__title {
  margin: 0;
  font-size: .9375em;           /* 15px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  color: var(--pui-text);
}

.pui-dropzone__hint {
  margin: 0;
  font-size: .8125em;           /* 13px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.35;
  color: var(--pui-muted);
}

/* --------------------------------------------------------------------------
   .pui-filelist / .pui-filerow — an upload list
   --------------------------------------------------------------------------
   Markup:
     <ul class="pui-filelist">
       <li class="pui-filerow">
         <span class="pui-filerow__thumb"><img src="…" alt=""></span>
         <div class="pui-filerow__main">
           <p class="pui-filerow__name" id="f1-name">HomeDepot-4471.pdf</p>
           <p class="pui-filerow__meta">1.2 MB · Uploading</p>
           <div class="pui-meter pui-meter--progress" role="progressbar"
                aria-labelledby="f1-name" aria-valuemin="0" aria-valuemax="100"
                aria-valuenow="62" aria-valuetext="Uploading, 62%"
                style="--pui-meter-value:62%">
             <span class="pui-meter__fill"></span>
           </div>
         </div>
         <span class="pui-filerow__state"><span class="pui-chip pui-chip--ok">62%</span></span>
       </li>
     </ul>

   ARIA:    __name carries the id the row's meter is labelled by — the
            progressbar has to announce WHICH file. A decorative thumbnail
            takes alt="" (it is a picture of the receipt the filename already
            names, not new information).
   Traps:
     • __name truncates with an ellipsis. That is visual only — the full
       string stays in the DOM and is read/selected/searched in full — but it
       means the element must contain the WHOLE filename, never a pre-
       shortened one.
     • A finished row still needs a text state ("Uploaded"), not just a green
       bar. Colour never carries the state alone.
     • Do not wrap the row in a <button>. It is a still life.
   -------------------------------------------------------------------------- */

.pui-filelist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pui-filerow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: .75em;
  padding: .75em 0;
  border-bottom: 1px solid var(--pui-rule);
}
.pui-filelist > .pui-filerow:last-child { border-bottom: 0; }

.pui-filerow__thumb {
  flex: none;
  display: grid;
  place-items: center;
  overflow: hidden;
  width: 2.5em;                 /* 40px                                      */
  height: 2.5em;
  border-radius: .5em;
  background: var(--pui-surface-2);
  color: var(--pui-muted);
}
.pui-filerow__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.pui-filerow__thumb svg { width: 1.25em; height: 1.25em; }

.pui-filerow__main { min-width: 0; }

.pui-filerow__name {
  margin: 0;
  overflow: hidden;
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-medium);
  line-height: 1.25;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--pui-text);
}

.pui-filerow__meta {
  margin: .15em 0 0;
  font-size: .75em;             /* 12px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-muted);
}

.pui-filerow__main .pui-meter { margin-top: .5em; }

.pui-filerow__state {
  display: flex;
  align-items: center;
  gap: .5em;
  justify-self: end;
}

/* ==========================================================================
   SMALL PARTS — avatar, empty state
   ========================================================================== */

/* --------------------------------------------------------------------------
   .pui-avatar
   --------------------------------------------------------------------------
   Markup (initials):  <span class="pui-avatar" aria-hidden="true">TB</span>
   Markup (photo):     <span class="pui-avatar"><img src="…" alt="Troy Brave"></span>

   ARIA:    initials are decoration — the person's name is in the row beside
            them, and "TB" announced on its own is noise. aria-hidden it. A
            PHOTO takes real alt text only when it is the only place the name
            appears; otherwise alt="".
   Sizes:   default 32px · --sm 24px · --lg 48px. The element sets its OWN
            font-size (that is the initials), so width/height are expressed in
            ems of THAT, not of the `.pui` root — 32 ÷ 12 = 2.6667em. Same
            arithmetic the rest of this file does; the px is in the comment.
            For a size that is not one of the three, set font-size and
            width/height together or the initials will not track the circle.
   Trap:    inside `.pui-avatars` each one gets a surface-coloured ring so the
            overlap reads. On a band that is not --pui-surface, re-point
            --pui-avatar-ring or the ring paints the wrong colour.
   Contrast: initials measure 4.9:1 dark / 4.7:1 light on their own fill.
   -------------------------------------------------------------------------- */

.pui-avatar {
  --pui-avatar-ring: var(--pui-surface);
  flex: none;
  display: inline-grid;
  place-items: center;
  overflow: hidden;
  width: 2.6667em;              /* 32px ÷ 12px                               */
  height: 2.6667em;
  border-radius: 50%;
  background: color-mix(in srgb, var(--pui-blue-text) 22%, var(--pui-mix-base));
  font-size: .75em;             /* 12px — the initials                       */
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: .01em;
  text-transform: uppercase;
  color: var(--pui-blue-text);
}
.pui-avatar img { width: 100%; height: 100%; object-fit: cover; }

.pui-avatar--sm {
  width: 2.4em;                 /* 24px ÷ 10px                               */
  height: 2.4em;
  font-size: .625em;            /* 10px                                      */
}
.pui-avatar--lg {
  width: 2.6667em;              /* 48px ÷ 18px                               */
  height: 2.6667em;
  font-size: 1.125em;           /* 18px                                      */
}

/* Overlapping row ("+3 more"). The ring is what stops it reading as a blob. */
.pui-avatars {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.pui-avatars > * + * { margin-left: -.5em; }
.pui-avatars .pui-avatar { box-shadow: 0 0 0 .1667em var(--pui-avatar-ring); } /* 2px */

/* --------------------------------------------------------------------------
   .pui-empty — nothing here yet
   --------------------------------------------------------------------------
   Markup:
     <div class="pui-empty">
       <svg class="pui-empty__icon" aria-hidden="true" …/>
       <p class="pui-empty__line">Inbox zero</p>
       <p class="pui-empty__sub">Every receipt is matched to a transaction.</p>
     </div>

   ARIA:    the icon is decoration — aria-hidden it and let the two lines say
            it. An empty state that is ONLY an icon says nothing out loud.
   Trap:    __sub is a sentence someone reads, so it uses --pui-muted (6.0:1
            on the light band), never --pui-dim. Do not "quiet it down" with
            opacity — that is how a 6:1 line becomes a 3:1 line.
   -------------------------------------------------------------------------- */

.pui-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4em;
  padding: 2em 1.25em;
  text-align: center;
}

.pui-empty__icon {
  width: 2em;                   /* 32px                                      */
  height: 2em;
  margin-bottom: .25em;
  color: var(--pui-muted);
}

.pui-empty__line {
  margin: 0;
  font-size: .9375em;           /* 15px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  color: var(--pui-text);
}

.pui-empty__sub {
  margin: 0;
  max-width: 34ch;
  font-size: .8125em;           /* 13px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.4;
  color: var(--pui-muted);
}

/* ==========================================================================
   CHARTS
   --------------------------------------------------------------------------
   THE PALETTE — read this before you paint anything
   --------------------------------------------------------------------------
   The six `--pui-cat-*` slots are a CATEGORICAL scale: they encode identity
   (which category), nothing else. Rules that come with them:

     1. Assign slots in FIXED ORDER, 1..6, never cycled and never re-ordered
        to "look better". The order is the colour-blindness safety mechanism:
        only touching slots are guaranteed separable, so slot 3 beside slot 4
        is safe and slot 1 beside slot 3 is not.
     2. A 7th category is not a 7th colour. Fold the tail into "Other" at
        slot 6, or facet.
     3. Colour follows the CATEGORY, not its rank. If a filter drops a
        category, the survivors keep their slots — they do not shuffle up.
     4. Never use --pui-accent-blue/-teal/-green/-gold for series. That is the
        four-tile decoration family; it was never checked for separation and
        two of its members collapse into each other under deuteranopia.
     5. Never paint TEXT with a slot. Values, labels and legend text wear
        --pui-text / --pui-muted; the coloured swatch beside them carries the
        identity. Several light slots are ~4.3:1 — fine for a mark, not a
        margin worth spending on 12px type.

   How they were derived (so the next person can re-derive, not guess):
     • Hues are the brand's own where the brand has one — blue 257° from
       --vos-blue/--vos-blue-ink, gold 78° from --vos-gold, green 158° from
       --vos-success, teal 222° from --pui-accent-teal nudged 6° cool because
       sRGB runs out of chroma at 216°. Violet 287° and magenta 352° are
       borrowed: the brand has four hue families and a categorical scale
       needs six.
     • Each mode is SELECTED, not dimmed: every slot is re-stepped to a
        uniform OKLCH lightness for its own surface — L ≈ .545 on white,
        L ≈ .660 on --pui-surface — so neither band is the other with the
        brightness turned down.
     • Measured on the ADJACENT pairlist (bars, stacked bars, donut arcs,
       legend rows — the forms this site draws): worst CVD ΔE 12.3 light /
       12.7 dark against a target of 8, worst normal-vision ΔE 17.2 light /
       17.3 dark against a floor of 15. Every slot clears 3:1 on its own
       surface (4.3:1 at worst) and sits inside its mode's lightness band
       with chroma ≥ .10.

   THE SERIES CAP — the one thing that will bite you:
     Those numbers are for marks that only ever touch their NEIGHBOURS. In a
     scatter, bubble or small-multiples chart any two marks can end up side by
     side, and there the six-slot set collapses (slot 1 blue ↔ slot 3 violet
     measure ΔE 0.9 under deuteranopia — indistinguishable). For those forms
     use slots 1, 4 and 5 ONLY (blue / magenta / gold: worst pair ΔE 12.3 CVD,
     19.1 normal, in both modes) and fold everything else into "Other".

   ONE MORE CAUTION: slot 2 is a green and slot 5 is a gold, and this app also
   uses green for money-in (--pui-pos) and amber for a warning chip. A green
   slice in a SPENDING chart can read as "the good one". Mitigate the way the
   app already does — every mark gets a text label and a legend entry, so the
   category is named and the colour is only a lookup key.

   Painting a mark: set --pui-mark on it, or add `.pui-cat-N`, and the bar,
   arc, swatch, sparkline and dot all pick it up.
   ========================================================================== */

.pui-cat-1 { --pui-mark: var(--pui-cat-1); }
.pui-cat-2 { --pui-mark: var(--pui-cat-2); }
.pui-cat-3 { --pui-mark: var(--pui-cat-3); }
.pui-cat-4 { --pui-mark: var(--pui-cat-4); }
.pui-cat-5 { --pui-mark: var(--pui-cat-5); }
.pui-cat-6 { --pui-mark: var(--pui-cat-6); }

/* --------------------------------------------------------------------------
   .pui-chart — the scaffolding around any mark
   --------------------------------------------------------------------------
   Markup:
     <figure class="pui-chart">
       <figcaption class="pui-chart__head">
         <h4 class="pui-chart__title">Spending by category</h4>
         <p class="pui-chart__note">Last 30 days · $48,210</p>
       </figcaption>
       <div class="pui-chart__plot"> …bars or donut… </div>
       <ul class="pui-legend"> … </ul>
     </figure>

   ARIA:    <figure> + a real <figcaption> is the whole accessibility story
            for a static chart. The figcaption must state the HEADLINE the
            chart makes ("Payroll is 41% of spend"), not just its title —
            that sentence is what a screen-reader user gets instead of the
            picture. Mark the plot itself aria-hidden="true": a div full of
            width percentages announces as gibberish.
   Trap:    do not put the plot inside the figcaption. A figcaption is the
            alternative to the graphic, not a wrapper for it.
   -------------------------------------------------------------------------- */

.pui-chart {
  display: flex;
  flex-direction: column;
  gap: .875em;
  min-width: 0;
  margin: 0;
}

.pui-chart__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .25em .75em;
}

.pui-chart__title {
  margin: 0;
  font-size: .875em;            /* 14px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.25;
  color: var(--pui-text);
}

.pui-chart__note {
  margin: 0;
  font-size: .75em;             /* 12px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  color: var(--pui-muted);
}

.pui-chart__plot { min-width: 0; }

/* --------------------------------------------------------------------------
   .pui-bars — horizontal category bars
   --------------------------------------------------------------------------
   The default form for "spending by category": the labels are words, and
   words fit beside a horizontal bar without rotating anything.

   Markup:
     <ul class="pui-bars" aria-hidden="true">
       <li class="pui-bar pui-cat-1" style="--pui-bar-value:41%">
         <span class="pui-bar__label">Payroll</span>
         <span class="pui-bar__track"><span class="pui-bar__fill"></span></span>
         <span class="pui-bar__value">$19,760</span>
       </li>
       …
     </ul>

   ARIA:    the list is aria-hidden and the numbers live in the figcaption or
            a `.pui-rows` beside it — otherwise a screen reader reads six
            labels with no values attached. If the bars ARE the only place
            the numbers appear, drop the aria-hidden: __label and __value are
            both real text, so the row reads fine as "Payroll $19,760".
   Knobs:   --pui-bar-value (percent of track) · --pui-bars-label (label
            column width, default 7em) · --pui-bar-h (bar thickness, 10px)
   Traps:
     • __value is never optional. A bar with no printed number is a shape.
     • The bar is capped at 10px, not stretched to fill the row — the air
       above and below it is what keeps six of them from reading as a block.
     • Do NOT colour __value with the slot. Text wears text tokens.
   -------------------------------------------------------------------------- */

.pui-bars {
  --pui-bars-label: 7em;
  display: flex;
  flex-direction: column;
  gap: .75em;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pui-bar {
  --pui-bar-h: .625em;          /* 10px                                      */
  display: grid;
  grid-template-columns: minmax(0, var(--pui-bars-label)) minmax(0, 1fr) auto;
  align-items: center;
  gap: .75em;
  min-width: 0;
}

.pui-bar__label {
  min-width: 0;
  overflow: hidden;
  font-size: .8125em;           /* 13px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.25;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--pui-muted);
}

.pui-bar__track {
  position: relative;
  height: var(--pui-bar-h);
  border-radius: calc(var(--pui-bar-h) / 2);
  background: var(--pui-mark-track);
}

.pui-bar__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--pui-bar-value, 0%);
  min-width: var(--pui-bar-h);
  border-radius: inherit;
  background: var(--pui-mark, var(--pui-cat-1));
}

.pui-bar__value {
  font-size: .8125em;           /* 13px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.25;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--pui-text);
}

/* --------------------------------------------------------------------------
   .pui-donut — one composition, drawn as a ring
   --------------------------------------------------------------------------
   Markup:
     <div class="pui-donut" aria-hidden="true" style="
          --pui-donut-stops:
            var(--pui-cat-1)   0    147.6deg,
            var(--pui-mark-gap) 147.6deg 149.6deg,
            var(--pui-cat-2) 149.6deg 234deg,
            …">
       <div class="pui-donut__center">
         <p class="pui-donut__value">$48,210</p>
         <p class="pui-donut__label">Last 30 days</p>
       </div>
     </div>

   ARIA:    aria-hidden on the ring; the centre figure is real text and is
            usually ALSO the section's hero number, so do not hide the
            __center — put the aria-hidden on the ring element only if the
            centre is duplicated elsewhere, otherwise leave the whole thing
            exposed (it reads as "$48,210 Last 30 days", which is correct).
   Knobs:   --pui-donut-stops (the conic stop list — you write it)
            --pui-donut-size (default 11em) · --pui-donut-thickness (22%)
   Traps:
     • The 2px separator between arcs is a `--pui-mark-gap` stop you author
       INTO the stop list — 2px of a 176px ring is ≈ 2deg. There is no
       border on an arc; a stroke around a slice is data-weight ink that is
       not data.
     • Stops are degrees, and the list must run 0 → 360 with no holes, or the
       browser interpolates a gradient where you wanted a hard edge.
     • A donut past ~5 slices stops being readable. Use `.pui-bars`.
   -------------------------------------------------------------------------- */

.pui-donut {
  --pui-donut-size: 11em;       /* 176px                                     */
  --pui-donut-thickness: 22%;
  position: relative;
  display: grid;
  place-items: center;
  width: var(--pui-donut-size);
  max-width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(from 0deg, var(--pui-donut-stops, var(--pui-mark-track) 0 360deg));
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - var(--pui-donut-thickness) * 2), #000 calc(100% - var(--pui-donut-thickness) * 2 + 1px));
  mask: radial-gradient(closest-side, transparent calc(100% - var(--pui-donut-thickness) * 2), #000 calc(100% - var(--pui-donut-thickness) * 2 + 1px));
}

/* The mask clips the ring, so the centre text has to escape it. */
.pui-donut__center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .1em;
  text-align: center;
  -webkit-mask: none;
  mask: none;
}

.pui-donut__value {
  margin: 0;
  font-size: 1.25em;            /* 20px                                      */
  font-weight: var(--fw-semibold);
  line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--pui-text);
}

.pui-donut__label {
  margin: 0;
  font-size: .75em;             /* 12px                                      */
  font-weight: var(--fw-regular);
  line-height: 1.2;
  color: var(--pui-muted);
}

/* A donut needs its centre text OUTSIDE the mask; wrapping it in this pair
   keeps the ring and the figure independent. */
.pui-donutwrap {
  position: relative;
  display: inline-grid;
  place-items: center;
}

/* --------------------------------------------------------------------------
   .pui-legend — mandatory for two or more series
   --------------------------------------------------------------------------
   Markup:
     <ul class="pui-legend">
       <li class="pui-legend__item pui-cat-1">
         <span class="pui-legend__swatch"></span>
         <span class="pui-legend__label">Payroll</span>
         <span class="pui-legend__value">41%</span>
       </li>
       …
     </ul>

   ARIA:    a real <ul>/<li>, NOT aria-hidden. The legend is the dependable
            identity channel — it is the part of the chart that still works
            without colour, so it is the last thing to hide.
   Modifier:
     .pui-legend--stack   one item per line (a donut's side panel)
   Traps:
     • Two or more marks means a legend is present. One mark means no legend
       — the title already names it.
     • __label wears --pui-text, never the slot colour.
     • __swatch is 10px and has no border; keep it, it is what makes the
       swatch column scan.
   -------------------------------------------------------------------------- */

.pui-legend {
  display: flex;
  flex-wrap: wrap;
  gap: .5em 1.25em;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pui-legend--stack {
  flex-direction: column;
  gap: .625em;
}

.pui-legend__item {
  display: flex;
  align-items: center;
  gap: .5em;
  min-width: 0;
  font-size: .8125em;           /* 13px                                      */
  line-height: 1.25;
}

.pui-legend__swatch {
  flex: none;
  width: .625em;                /* 10px                                      */
  height: .625em;
  border-radius: .1875em;
  background: var(--pui-mark, var(--pui-cat-1));
}

.pui-legend__label {
  min-width: 0;
  overflow: hidden;
  font-weight: var(--fw-regular);
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--pui-text);
}

.pui-legend__value {
  margin-left: auto;
  padding-left: .5em;
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--pui-muted);
}

/* --------------------------------------------------------------------------
   .pui-sparkline — a 12-point trend, no axes
   --------------------------------------------------------------------------
   Markup:
     <span class="pui-sparkline pui-cat-1" aria-hidden="true">
       <svg viewBox="0 0 100 28" preserveAspectRatio="none">
         <path class="pui-sparkline__area" d="M0,20 L20,14 … L100,28 L0,28 Z"/>
         <path class="pui-sparkline__line" d="M0,20 L20,14 … L100,6"/>
       </svg>
       <span class="pui-sparkline__dot"></span>
     </span>

   ARIA:    always aria-hidden. A sparkline has no axis and no labels, so it
            has nothing to say out loud; the number it sits beside is the
            content. Never let it be the only place a trend appears.
   Knobs:   --pui-spark-w (default 5em) · --pui-spark-h (default 1.75em)
   Traps:
     • preserveAspectRatio="none" is required — the viewBox is stretched to
       the box and a sparkline drawn to scale ends up a flat line.
     • vector-effect keeps the 2px stroke 2px after that stretch. Without it
       the line goes thin horizontally and fat vertically.
     • The end dot carries a surface ring so it stays legible where it lands
       on the line; on a band that is not --pui-surface, re-point
       --pui-spark-ring.
   -------------------------------------------------------------------------- */

.pui-sparkline {
  --pui-spark-w: 5em;           /* 80px                                      */
  --pui-spark-h: 1.75em;        /* 28px                                      */
  --pui-spark-ring: var(--pui-surface);
  position: relative;
  display: inline-block;
  width: var(--pui-spark-w);
  height: var(--pui-spark-h);
  vertical-align: middle;
}
.pui-sparkline svg { display: block; width: 100%; height: 100%; overflow: visible; }

.pui-sparkline__line {
  fill: none;
  stroke: var(--pui-mark, var(--pui-cat-1));
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.pui-sparkline__area {
  fill: var(--pui-mark, var(--pui-cat-1));
  opacity: .10;
  stroke: none;
}

/* Positioned by the caller with --pui-spark-x / --pui-spark-y (percentages
   of the box), because only the caller knows where the series ends. */
.pui-sparkline__dot {
  position: absolute;
  left: var(--pui-spark-x, 100%);
  top: var(--pui-spark-y, 0%);
  width: .5em;                  /* 8px                                       */
  height: .5em;
  margin: -.25em 0 0 -.25em;
  border-radius: 50%;
  background: var(--pui-mark, var(--pui-cat-1));
  box-shadow: 0 0 0 .125em var(--pui-spark-ring);
}

/* --------------------------------------------------------------------------
   Narrow product shots: let the KPI row fold instead of squeezing to mush.
   Driven by the panel's own width, not the viewport.
   -------------------------------------------------------------------------- */

/* A shrunken app keeps four tiles across for as long as it can — that row is
   the recognisable silhouette — so the headline figure gives ground first,
   which is what stops the value and its delta breaking onto two lines. */
@container (max-width: 790px) {
  .pui-app { padding: 1.25em 1.15em 1.3em; gap: 1.2em; }
  .pui-tiles { gap: .6em; }
  .pui-tile { padding: .7em .6em .8em; }
  .pui-tile__head { gap: .5em; }
  .pui-tile__icon { width: 1.6em; height: 1.6em; }
  .pui-metric__value { font-size: 1.2em; gap: .2em .3em; }
  .pui-delta { padding: .18em .4em; }
}
/* Folded to two across, each tile is roomy again — take the full size back. */
@container (max-width: 560px) {
  .pui-tiles { --pui-tile-cols: 2; }
  .pui-tile { padding: .75em .75em .85em; }
  .pui-tile__head { gap: .6em; }
  .pui-tile__icon { width: 1.85em; height: 1.85em; }
  .pui-metric__value { font-size: 1.3em; gap: .25em .4em; }
}
@container (max-width: 320px) {
  .pui-tiles { --pui-tile-cols: 1; }
}

@supports not (container-type: inline-size) {
  .pui-tiles { --pui-tile-cols: 2; }
}
