/* ==========================================================================
   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`. Everything added after them — panel, card,
         statusframe, btn, dropzone, filerow, avatar, empty, and the whole
         chart set — is on that same @16px calibration.
     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.

   --------------------------------------------------------------------------
   PORTING A SURFACE: app value → nearest passing kit token
   --------------------------------------------------------------------------
   Four sections ported four product surfaces and each one independently
   discovered that some of the app's own colours do not clear AA once they are
   printed at marketing scale on a marketing band. Each solved it differently.
   This is the shared answer. When the app hands you a value in the LEFT
   column, paint the RIGHT one and say so at the use site.

     APP VALUE (measured)                       USE INSTEAD
     ------------------------------------------ -----------------------------
     ew-brand-text #4A8FD4 as text on white     --pui-blue-text  (6.6:1)
       3.06:1 — fails AA
     --vos-blue #50a1f9 as text on white        --pui-blue-text  (6.6:1)
       2.7:1 — never text on a light band
     --vos-mint-bright #5ffcc0 on white         --pui-accent-mint (light col
       1.3:1                                      darkens it to 5.4:1)
     dark:text-white/68 on an atmosphere        --pui-muted      (6.6:1)
       4.3:1 at 12px
     dark:text-white/44 on an atmosphere        --pui-muted      (6.6:1)
       4.2:1
     amber-500 #f59e0b as text on white         --pui-attention-ink (6.4:1)
       2.15:1
     --chart-increase/-decrease as text         --pui-spend-up / --pui-spend-
       the FILL tokens are marks, not ink         down  (>=6:1 both columns)
     --chart-comparison (muted-fg mix)          --pui-mark-neutral (5.0:1 dark
       too faint to read as an arc                / 5.5:1 light)
     account-tile #34d17f `Connected`           --pui-tile-h-connected
       2.0:1 on the LIGHT tile                    (4.9:1 light, 8.6:1 dark)
     text-error-primary                         UNDEFINED IN THE PRODUCT. It
       renders grey, not red                      has no rule; the app really
                                                  does ship it grey. Use
                                                  --pui-muted and say so, or
                                                  --pui-down if the section
                                                  is depicting the intent.
     border-slate-300/35 on an atmosphere       --pui-line-strong (3.0:1)
       1.05:1 — the card stops existing
     --chart-readiness at 10%/45%               --pui-attention-* or a 70%
       ~1.4:1 boundary                            --pui-blue-text mix (3.4:1)
     account-tile #f0475f `Disconnected`        --pui-tile-h-disconnected
       4.19:1 on the DARK tile's lightest         (4.98:1 dark / 5.53:1 light)
       paper stop — under AA at 9px
     account-brand-mark #59708f monogram        --pui-tile-mark (4.67:1 on the
       4.07:1 on the LIGHT rail's top stop        rail's own top stop)
     white/.10 or slate/.16 as the SELECTED     `.pui-seg__item.is-active`, a
       segment's edge — 1.27:1 dark, 1.41:1       70% --pui-blue-text mix
       light, so the current view is unmarked     (3.90:1 dark / 3.21:1 light)
     white/[0.075] rule inside a T-chart        --pui-tchart-rule (1.61:1 dark
       1.21:1 on an atmosphere — the two          / 1.37:1 light; and see its
       halves read as one block                   note on why a pure GROUPING
                                                  rule is exempt from 3:1)
     blue-100/blue-700 `<Badge variant=         `.pui-chip--outline` through
       "outline">` — no dark-column answer        --pui-badge-* (5.25:1 dark /
       exists in the product at all               5.49:1 light ink)

   The rule underneath all of these: the product renders at 100% on a device
   the user controls; a product SHOT renders at 60-70% inside a marketing band
   with its own glows behind it. A value that is merely adequate in the app is
   not adequate here. Step it, keep the hue, and cite the measurement.

   --------------------------------------------------------------------------
   UNCONSUMED PRIMITIVES — audited this round, kept on purpose
   --------------------------------------------------------------------------
   `.pui-greeting`, `.pui-datestamp` and `.pui-tiles`/`.pui-tile*` were drawn
   for the Figma dashboard PLATFORM-PORT-SPEC §0.1 proved is not in the product
   (no greeting, no pulse control, no KPI row). No section renders any of them
   today. They are not deleted — `.pui-tile` is a real app shape a later port
   will want — but nothing on the site exercises them, so the first section to
   use one owns re-measuring it.
   `.pui-seg` IS consumed (travel), and `.pui-app` / `.pui-appbar` are consumed
   (dashboard / spending-analysis). Those three are load-bearing.
   ========================================================================== */

.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     */
  /* CONTAINER OUTLINE. This is the ONLY thing that says where a `.pui-card`
     or a `.pui-tablewrap` ends, and it has to hold in three places at once:
     against the card's own --pui-surface, against the app canvas --pui-bg,
     and against the marketing band --bg-deep / --bg-deep-2 behind a bare
     shot. The sampled app value #283541 measured 1.44:1 / 1.61:1 / 1.60:1 —
     i.e. it was not there. Two porting agents hit this independently and one
     abandoned `.pui-card` entirely. #556680 is the same desaturated navy
     ramped until it clears the 3:1 non-text minimum in all three:
       3.10:1 on --pui-surface · 3.45:1 on --pui-bg · 3.43:1 on --bg-deep
     Do not soften it back toward the app's swatch. The app draws this card
     at 100% on a screen the reader controls; the site draws it at ~65%
     inside a band with two navy glows behind it. */
  --pui-line-strong: #556680;   /* container outlines                        */
  --pui-stroke: rgba(255, 255, 255, .06);
  --pui-stroke-strong: rgba(255, 255, 255, .10);
  /* CONTROL EDGE. --pui-stroke-strong at .10 white is 1.24:1 on --bg-deep and
     1.31:1 on --pui-surface — fine as a decorative inner seam, useless as the
     boundary of a control. A button IS a UI component, so its edge is held to
     3:1 wherever it lands: 3.23:1 on --bg-deep, 3.33:1 on --pui-surface. */
  --pui-btn-edge: rgba(255, 255, 255, .36);
  --pui-btn-bg: var(--pui-chip-bg);
  /* 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);
  /* GROUPING rule — the divider inside a `.pui-tchart`, and the one hairline
     in this file that is deliberately NOT held to 3:1.
     1.4.11 covers a boundary you must see to operate or to read the graphic.
     A T-chart's divider is neither: both halves carry their own label and
     their own figure, so removing the line loses a visual grouping and no
     information. Holding it to 3:1 would draw a hard box through the middle
     of an atmosphere block and the app draws nothing like that.
     It still gets a property, and it is still stepped up from the app's
     white/[0.075] (1.21:1 on an atmosphere) to --pui-rule, which measures
     1.61:1 dark / 1.37:1 light. Re-point it if a caller needs more. */
  --pui-tchart-rule: var(--pui-rule);
  /* 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);
  /* SPEND POLARITY. --pui-up is green and --pui-down is red because the
     default reading of a delta is "the number got better / worse". On a
     SPENDING surface that reading inverts — spending up is the bad news — and
     the spending-analysis port had to scope its own pair because flipping
     --pui-up/--pui-down would have silently inverted every other delta on the
     site. These are the app's own --chart-increase-text / --chart-decrease-
     text, both columns, and they are reached through `.pui-delta--spend`
     rather than by redefining the existing semantics.
       dark  #ffc266 11.3:1 · #65d6aa 10.1:1  on --pui-surface
       light #8a4b00  6.8:1 · #006b4f  6.5:1  on white
     Text on its own tinted pill measures 8.4 / 7.6 dark, 6.0 / 5.8 light. */
  --pui-spend-up: #ffc266;      /* spending ROSE — the bad direction         */
  --pui-spend-up-bg: rgba(255, 194, 102, .14);
  --pui-spend-down: #65d6aa;    /* spending FELL                             */
  --pui-spend-down-bg: rgba(101, 214, 170, .14);
  /* 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-warn: #e8b33a;          /* amber "needs a look" chip                 */
  --pui-alert: #881523;         /* count badge on a review queue             */
  --pui-chip-bg: rgba(255, 255, 255, .08);
  /* The same chip on a BARE readout. .08 white is calibrated for
     --pui-surface (1.24:1 there — quiet, which is the intent inside a card).
     Over a bare band it composites to 1.17:1 on --bg-deep and the pill shape
     is simply gone: the label still reads, but the component has stopped
     being a chip. .36 restores the shape at 3.23:1 while the label sits at
     5.4:1 on the fill. `.pui--bare` swaps --pui-chip-bg to this; it is a
     separate property (not a rule in `--bare`) so the LIGHT column can carry
     its own value through the same swap. */
  --pui-chip-bg-bare: rgba(255, 255, 255, .36);
  /* Chip INK. These were hard-coded inside the four chip rules, which broke
     the file's own "no colour outside a property" rule and cost us exactly
     the bug it exists to prevent: --pui-good's fill darkens on the light band
     and its near-black ink went with it, landing at 3.27:1. */
  --pui-chip-ok-ink: #ffffff;   /* on --pui-info   — 4.86:1                  */
  --pui-chip-good-ink: #04150f; /* on --pui-pos    — 9.68:1 dark             */
  --pui-chip-warn-ink: #1b1403; /* on --pui-warn   — 9.53:1 both bands       */
  --pui-chip-alert-ink: #ffffff;/* on --pui-alert  — 9.64:1 both bands       */
  /* OUTLINE BADGE (`.pui-chip--outline`). A third chip shape, not a fifth
     colour: `<Badge variant="outline">` is a tinted plate with a real stroke
     around it, and the stroke is the whole point — it is what says "status"
     rather than "tag". The light column is the product's own
     blue-100 / blue-700 pair (trip-status-badge.tsx:6-11). The DARK column has
     no product answer at all — the app never renders this badge on a dark
     surface — so it is derived here from --pui-blue-text the same way
     `.pui-btn--primary` derives its plate.
       dark  ink 5.25:1 on its own plate · edge 4.30:1 on --pui-surface
       light ink 5.49:1 on its own plate · edge 3.51:1 on white
     The PLATE is deliberately quiet (1.28:1 dark / 1.22:1 light). It is a
     tint, not the boundary; the edge is the boundary. */
  --pui-badge-ink: var(--pui-blue-text);
  --pui-badge-bg: color-mix(in srgb, var(--pui-blue-text) 18%, var(--pui-mix-base));
  --pui-badge-edge: color-mix(in srgb, var(--pui-blue-text) 70%, transparent);

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

  /* ---- Attention (amber) ------------------------------------------------ */
  /* An "attention" card is a block the app tints amber because it wants a
     look, not because anything is wrong (`Suggested trip expenses (4)`).
     Four values, because a tinted block needs all four and getting any one
     of them from the app's raw swatch fails:
       __bg    a wash. It is NOT the separation — 1.19:1 dark / 1.06:1 light
               against the surface it sits on, and it is meant to be.
       __edge  the separation. 5.8:1 dark / 3.5:1 light.
       __ink   the label. amber-500 #f59e0b is 2.15:1 on white and unusable
               as text; --pui-warn is 7.9:1 on the dark wash and the light
               column swaps in the app's own --chart-increase-text at 6.4:1.
       __rule  the divider between stacked rows inside the block. */
  --pui-attention-bg: rgba(232, 179, 58, .10);
  --pui-attention-edge: #b98a24;
  --pui-attention-ink: var(--pui-warn);
  --pui-attention-rule: rgba(232, 179, 58, .28);

  /* 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);
  /* NEUTRAL MARK — the folded "Other" category, and nothing else.
     Palette rule 2 says a 7th category is not a 7th colour: fold the tail into
     "Other". But "Other" then has to be PAINTED, and it must not read as a
     seventh identity. --pui-mark-track is the only neutral the kit had and it
     is a track: 1.6:1, invisible as an arc. This is the neutral at mark
     weight — deliberately achromatic so it reads as "the rest", and dark
     enough (light column) / light enough (dark column) to clear 3:1 on both
     its own surface and the track it can be drawn over.
       dark  #7c8794  4.95:1 on --pui-surface · 3.08:1 on --pui-mark-track
       light #616a76  5.48:1 on white          · 3.41:1 on --lt-track
     It is NOT a 7th slot. Never assign it to a real category, and never use
     it when six or fewer categories are in play. */
  --pui-mark-neutral: #7c8794;
  /* CURRENT-SERIES STROKE — the total/trend line drawn OVER a categorical
     chart. It is structure, not identity, so it is achromatic on purpose:
     that is the only way it can never be confused with a slot, whatever
     number of slots are lit. A 7th hue here would collide with slot 1 under
     deuteranopia and would also imply the line is a category.
       dark  #cdd6e4  12.4:1 on --pui-surface
       light #244068  10.5:1 on white  (--vos-navy-deep)
     Use it for the stroke, the end dots and the legend rule; the swatch for
     it is `.pui-legend__line`, never `.pui-legend__swatch`. */
  --pui-mark-trend: #cdd6e4;

  /* ---- Containers -------------------------------------------------------- */
  --pui-shadow: 0 .5em 2em rgba(0, 0, 0, .35);
  /* The lift an IN-APP container (tile, card) needs to separate from what is
     behind it.
     This shipped as `none` on the dark band on the theory that "--pui-surface
     is two steps off --pui-bg and the hairline finishes the job". Measured,
     neither half of that was true: --pui-surface is 1.11:1 on --pui-bg and
     1.10:1 on --bg-deep, and the hairline was 1.60:1. A `.pui-card` on the
     dark column was a rectangle of nothing — the same defect the light column
     already had a fix for.
     The boundary is now carried by --pui-line-strong at 3:1 (see above). This
     property carries the LIFT, and on a #05080f band a lift cannot be a cast
     shadow: there is no darker to cast into. So the dark value is the
     specular one — light catching the container's top edge, the same device
     the product's own AccountTile uses — and the light value stays the cast
     shadow, because on white that is what a lift looks like. */
  --pui-raise: inset 0 1px 0 rgba(255, 255, 255, .07);

  /* ---- Financial atmospheres (see `.pui-atmos`) -------------------------- */
  /* Straight from packages/ui-kit/src/lib/financial-atmosphere.ts, both
     columns, verbatim — a flat 135deg ramp with ONE soft radial on top. They
     are transcribed rather than approximated because "the area looked soft"
     is exactly how a linear ramp becomes an invented radial glow. */
  --pui-atmos-base: linear-gradient(135deg, #090B12 0%, #10141D 58%, #161B27 100%);
  --pui-atmos-exec: radial-gradient(circle at top left, rgba(96, 165, 250, .08), transparent 30%);
  --pui-atmos-action: radial-gradient(circle at top right, rgba(96, 165, 250, .10), transparent 31%);
  --pui-atmos-ledger: radial-gradient(circle at bottom right, rgba(20, 184, 166, .055), transparent 34%);
  --pui-atmos-reserve: radial-gradient(circle at top, rgba(148, 163, 184, .06), transparent 34%);
  --pui-atmos-vignette: radial-gradient(ellipse at center, transparent 60%, rgba(4, 6, 11, .58) 100%);
  /* DELIBERATE DIVERGENCE FROM THE PRODUCT, and the measurement that forces
     it: an atmosphere is 1.09:1 against --pui-bg on dark and 1.04:1 against
     white on light. In the app that is fine — the block sits inside an app
     canvas that is itself one step off it, and the vignette does the rest. On
     a marketing band there is no canvas, so the block has no edge at all. The
     app's own light border (slate-200/80) measures 1.18:1 and the dark column
     draws none. This points at --pui-line-strong (3:1 both columns) so the
     region reads as a region. A section reproducing an atmosphere that really
     is inside an app canvas can set it back to `transparent`. */
  --pui-atmos-edge: var(--pui-line-strong);
  --pui-atmos-shadow: none;

  /* ---- AccountTile (see `.pui-accounttile`) ------------------------------ */
  /* Card stock, not app chrome — a deliberately warmer, lighter palette than
     --pui-surface, because the object is meant to look like a printed card
     lying on the canvas. Ported from
     packages/ui-kit/src/components/accounts/account-tile.tsx (size="compact").
     Every value goes through a property so `.pui--light` can re-point the
     whole object, which matters more here than anywhere else in the kit: the
     tile is a LIGHT object in the product and its dark skin is the derivative.
     Contrast, worst case across the tile's own gradient stops:
       __name  15.3:1 light · 14.2:1 dark
       __mask/__cur/__cents/__comma  4.5:1 light · 6.2:1 dark            */
  --pui-tile-bg:
    radial-gradient(170% 130% at 10% -16%, rgba(96, 165, 250, .16), transparent 52%),
    linear-gradient(158deg, #1a2230, #111826 48%, #0a0f1a);
  --pui-tile-ring: rgba(148, 170, 205, .09);
  --pui-tile-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .05),
    0 14px 27px -14px rgba(0, 0, 0, .85),
    0 2px 8px -5px rgba(0, 0, 0, .7);
  --pui-tile-rail-bg: linear-gradient(100deg, rgba(6, 10, 17, .72), rgba(11, 17, 27, .34) 72%, rgba(4, 8, 14, .6));
  --pui-tile-rail-shadow: inset 1px 1px 0 rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .03);
  --pui-tile-rail-edge: linear-gradient(90deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, .5));
  --pui-tile-mark: #a8bcd8;      /* 9.8:1 on the rail's own top stop         */
  --pui-tile-mark-shadow: 0 1px 3px rgba(0, 0, 0, .6);
  --pui-tile-type: #b9c7dd;
  --pui-tile-body-bg: linear-gradient(165deg, #1d2634, #141c2a 55%, #0d131f);
  --pui-tile-body-edge: linear-gradient(180deg, rgba(196, 216, 246, .02), rgba(196, 216, 246, .32) 22%, rgba(196, 216, 246, .16) 68%, rgba(196, 216, 246, .02));
  --pui-tile-ghost-stroke: #9fc0ee;
  --pui-tile-ghost-blend: screen;
  --pui-tile-ghost-filter: none;
  --pui-tile-name: #f4f7fc;
  --pui-tile-meta: #9aa6bd;
  --pui-tile-figure: #f4f7fc;
  --pui-tile-figure-stroke: #090d15;
  /* HEALTH — and the one place this kit knowingly does NOT match the product.
     account-tile.tsx:29-34 gives each state ONE hex for both themes. On the
     light tile that hex is the light column's problem and it loses:
       #34d17f Connected 1.9:1 · #16c5c0 Manual 2.0:1 · #f4c010 Needs
       attention 1.6:1 · #f0475f Disconnected 3.4:1
     — all four under AA for a 9px extra-bold uppercase label, three of them
     badly. The dark column's values already clear 7:1, so the DARK column is
     the app's own and only the LIGHT column is stepped, same hue, to >=4.5:1
     on the tile's darkest paper stop (#e7effb):
       connected 4.49 · manual 4.76 · attention 5.44 · disconnected 5.53
     If the product ever fixes its own tile, these are the values to take.

     ONE OF THE FOUR IS ALSO STEPPED ON THE DARK COLUMN, and it is the one the
     note above got wrong: #f0475f measures 4.19:1 on the dark tile's LIGHTEST
     paper stop (#1d2634, the top-left of the body's 165deg ramp — which is
     exactly where the health label is pinned). 4.19 is under AA for a 9px
     extra-bold label, so the dark column does not in fact "already clear 7:1"
     for all four. It is lightened one step on the same hue to #f4636f. */
  --pui-tile-h-connected: #34d17f;     /* 7.67:1 worst, dark paper           */
  --pui-tile-h-manual: #16c5c0;        /* 7.09:1                             */
  --pui-tile-h-attention: #f4c010;     /* 8.99:1                             */
  --pui-tile-h-disconnected: #f4636f;  /* 4.98:1 — app ships #f0475f, 4.19:1 */

  --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;
  /* Deepened from #f7f9fc: the entity switcher and the segmented control both
     paint this as an INSET well, and at 1.05:1 against the card the well was
     not there at all. */
  --pui-surface-sunk: #eef2f8;
  --pui-line: var(--lt-rule-navy);
  /* NOT --lt-stroke. That is rgba(11,18,32,.10), which composites to #e7e7e9
     and measures 1.23:1 on white — the light `.pui-card` only ever read
     because --pui-raise supplies a shadow, and a `.pui-tablewrap` inside a
     white app canvas had nothing at all. #848a94 is the same neutral ramped
     to the 3:1 non-text minimum against every light surface the kit paints:
       3.40:1 on #ffffff · 3.13:1 on --pui-surface-2 · 3.02:1 on
       --pui-surface-sunk
     --lt-stroke is still right for --pui-stroke (a decorative inner seam). */
  --pui-line-strong: #848a94;
  --pui-stroke: var(--lt-stroke);
  --pui-stroke-strong: rgba(11, 18, 32, .16);
  --pui-btn-edge: rgba(11, 18, 32, .46);   /* 3.12:1 on white               */
  --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);
  /* The app's own --chart-increase-text / --chart-decrease-text, light column
     (packages/ui-kit/src/styles/globals.css:61-62). 6.8:1 / 6.5:1 on white;
     6.0:1 / 5.8:1 on their own tinted pills. */
  --pui-spend-up: #8a4b00;
  --pui-spend-up-bg: rgba(245, 158, 11, .16);
  --pui-spend-down: #006b4f;
  --pui-spend-down-bg: rgba(0, 212, 146, .16);

  --pui-attention-bg: rgba(245, 158, 11, .07);
  --pui-attention-edge: #b3801f;        /* 3.49:1 on white                   */
  --pui-attention-ink: #8a4b00;         /* 6.44:1 on the wash                */
  --pui-attention-rule: rgba(179, 128, 31, .28);

  --pui-chip-bg: rgba(11, 18, 32, .07);
  --pui-chip-bg-bare: rgba(11, 18, 32, .46);  /* 3.12:1 on white            */
  /* --pui-pos darkens to #007550 on this band, so the dark ink the chip wears
     on the dark band lands at 3.27:1 here. White is 5.74:1. */
  --pui-chip-good-ink: #ffffff;
  /* The product's OWN outline badge — trip-status-badge.tsx:6-11, literal.
     #1d4ed8 on #dbeafe is 5.49:1; the edge at .65 is 3.51:1 on white. Unlike
     the rest of this column these are not derived from a --vos-* token,
     because the badge is the one component where the app ships a light pair
     and no dark one — so here the app is the source and the dark column above
     is the derivative. */
  --pui-badge-ink: #1d4ed8;
  --pui-badge-bg: #dbeafe;
  --pui-badge-edge: rgba(29, 78, 216, .65);

  /* 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;
  /* 5.48:1 on white, 3.41:1 on --lt-track. See the dark note for why "Other"
     gets a neutral rather than a seventh hue. */
  --pui-mark-neutral: #616a76;
  /* --vos-navy-deep. 10.5:1 on white, and achromatic-adjacent enough that it
     can never be mistaken for a categorical slot. */
  --pui-mark-trend: var(--vos-navy-deep);

  --pui-shadow: 0 .25em 2.2em rgba(11, 18, 32, .10);
  --pui-raise: 0 1px 2px rgba(11, 18, 32, .06), 0 2px 10px rgba(11, 18, 32, .05);

  /* Atmospheres, light column — financial-atmosphere.ts, verbatim. */
  --pui-atmos-base: linear-gradient(135deg, #FFFFFF 0%, #F8FBFF 58%, #EEF4FA 100%);
  --pui-atmos-exec: radial-gradient(circle at top left, rgba(1, 111, 255, .06), transparent 31%);
  --pui-atmos-action: radial-gradient(circle at top right, rgba(1, 111, 255, .07), transparent 32%);
  --pui-atmos-ledger: radial-gradient(circle at bottom right, rgba(20, 184, 166, .05), transparent 34%);
  --pui-atmos-reserve: radial-gradient(circle at top, rgba(148, 163, 184, .055), transparent 34%);
  --pui-atmos-vignette: radial-gradient(ellipse at center, transparent 63%, rgba(148, 163, 184, .16) 100%);
  --pui-atmos-shadow: 0 1px 2px rgba(148, 163, 184, .35);

  /* AccountTile, light column — account-tile.tsx:313-321, 344-352, 361-390.
     This is the tile's NATIVE column: the dark skin above is the derivative. */
  --pui-tile-bg:
    radial-gradient(170% 130% at 10% -16%, rgba(37, 99, 235, .10), transparent 52%),
    linear-gradient(158deg, #ffffff, #f4f8ff 48%, #e7effb);
  --pui-tile-ring: rgba(49, 76, 118, .14);
  --pui-tile-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .9),
    0 1px 0 rgba(20, 60, 120, .04),
    0 14px 27px -14px rgba(20, 40, 80, .32),
    0 2px 8px -5px rgba(20, 40, 80, .24);
  --pui-tile-rail-bg: linear-gradient(100deg, rgba(220, 231, 247, .92), rgba(242, 247, 255, .78) 72%, rgba(213, 226, 244, .9));
  --pui-tile-rail-shadow: inset 1px 1px 0 rgba(72, 99, 138, .20), inset 0 1px 0 rgba(255, 255, 255, .65);
  --pui-tile-rail-edge: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(74, 94, 125, .24));
  /* The app's #59708f measures 4.07:1 on the rail's OWN top stop (rgba(220,
     231,247,.92) over near-white paper ≈ #dde7f7) — and the top stop is where
     the monogram sits. Under AA for a 12px extra-bold label. Stepped one notch
     on the same hue: 4.67:1 there, 5.42:1 further down the rail. */
  --pui-tile-mark: #516781;
  --pui-tile-mark-shadow: 0 1px 3px rgba(45, 62, 88, .24);
  --pui-tile-type: #55637a;
  --pui-tile-body-bg: linear-gradient(165deg, #ffffff, #f4f8ff 55%, #e9f0fa);
  --pui-tile-body-edge: linear-gradient(180deg, rgba(74, 105, 149, .02), rgba(74, 105, 149, .26) 22%, rgba(74, 105, 149, .12) 68%, rgba(74, 105, 149, .02));
  --pui-tile-ghost-stroke: #476992;
  --pui-tile-ghost-blend: multiply;
  --pui-tile-ghost-filter: saturate(.6) brightness(.94);
  --pui-tile-name: #111827;
  --pui-tile-meta: #5f6d84;      /* app ships #65758c at 4.05:1 — stepped    */
  --pui-tile-figure: #0c1220;
  --pui-tile-figure-stroke: rgb(244, 248, 255);
  /* Stepped for AA on the tile's own paper — see the dark-column note. */
  --pui-tile-h-connected: #0d7d45;     /* 4.49:1 worst paper stop            */
  --pui-tile-h-manual: #0c7573;        /* 4.76:1                             */
  --pui-tile-h-attention: #7d5a06;     /* 5.44:1                             */
  --pui-tile-h-disconnected: #b81f31;  /* 5.53:1                             */

  --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);
}

/* --------------------------------------------------------------------------
   `.pui--bare` — 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.

   It re-points properties only, like `.pui--light`, and it is declared AFTER
   `.pui--light` on purpose: `class="pui pui--light pui--bare"` has to take the
   light column's values for everything `--bare` does not touch, and `--bare`'s
   own swaps must win. Every swap below therefore reads through a property the
   light column also re-points — never a literal colour, or a light bare panel
   would inherit the dark column's answer.

   STILL THE CALLER'S JOB: --pui-meter-ring, --pui-avatar-ring and
   --pui-spark-ring. Those rings paint over the component's OWN fill (the meter
   fill, the neighbouring avatar, the sparkline), not over the band, so there
   is no band-derived value the kit could substitute. Re-point them to whatever
   the panel is actually sitting on.
   -------------------------------------------------------------------------- */
.pui--bare {
  --pui-bg: transparent;
  /* .08 white / .07 ink is calibrated for --pui-surface. Over a bare band it
     composites to 1.17:1 and the pill stops being a pill. */
  --pui-chip-bg: var(--pui-chip-bg-bare);
  /* The button keeps its 3:1 edge and drops the fill: a 3:1 plate behind a
     3:1 outline is two boundaries doing one job. */
  --pui-btn-bg: transparent;
  /* A donut's arc separators are the SURFACE showing through. On a bare panel
     there is no surface — the band is showing through — and `transparent` in
     a hard-stop conic gradient renders exactly that. Same pixels a correctly
     hand-matched band colour would give, without the caller having to match
     it. Re-point it if the donut sits on something that is not the band. */
  --pui-mark-gap: transparent;
}

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

/* --------------------------------------------------------------------------
   .pui-app — the app's own gutter and vertical rhythm
   --------------------------------------------------------------------------
   RE-CALIBRATED. The 1.4em/1.667em padding and 1.35em gap were cut from a
   Figma dashboard that does not exist in the product (PLATFORM-PORT-SPEC §0.1
   verified the greeting, the pulse control, the KPI row and the review queue
   are all inventions). The real client home is `p-3 sm:p-4` with
   `space-y-4 lg:space-y-5` — 16px of gutter and 20px of rhythm — so that is
   what the default is now, and the dashboard port no longer has to override
   the shell it is supposed to be reusing.

   Knobs:  --pui-app-pad   gutter, all four sides. Default 1em (16px).
           --pui-app-pad-top   top gutter only, for a surface whose first
                               child is a bar that already has its own air.
                               Defaults to --pui-app-pad.
           --pui-app-gap   vertical rhythm between bands. Default 1.25em.
   The narrow-panel container query below tightens the KNOBS, not `padding`
   and `gap` directly, so a caller's own value still wins where it should.
   -------------------------------------------------------------------------- */

.pui-app {
  --pui-app-pad: 1em;           /* 16px — home-content.tsx `p-4`              */
  --pui-app-gap: 1.25em;        /* 20px — `lg:space-y-5`                      */
  display: flex;
  flex-direction: column;
  gap: var(--pui-app-gap);
  padding: var(--pui-app-pad-top, var(--pui-app-pad)) var(--pui-app-pad) var(--pui-app-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; }
/* THE SELECTED VIEW. This is the one state in the kit that a reader has to be
   able to find, and the app's device for it — a fill one step off the well —
   does not survive being printed at 65%:
     dark   --pui-surface-2 on --pui-surface-sunk  1.09:1
     light  #f2f6fb on #eef2f8                     1.04:1
   and the hairline that was supposed to finish the job was --pui-stroke-strong,
   1.27:1 dark / 1.41:1 light over the well. So the fill said nothing and the
   edge said nothing: `.pui-seg` shipped with no identifiable selection.
   The travel port hit this and reached for the kit's OWN documented boundary
   device — 70% of --pui-blue-text, the same mix `.pui-meterframe` and
   `.pui-dropzone` use — which is promoted here rather than left section-local.
     3.90:1 dark on the active fill · 3.21:1 light on the well
   The fill is KEPT. Two channels for one state is the point: the edge carries
   the 3:1 and the fill carries the familiar app look. Do not soften the edge
   back toward --pui-stroke-strong. */
.pui-seg__item.is-active {
  border-color: color-mix(in srgb, var(--pui-blue-text) 70%, transparent);
  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);
  box-shadow: var(--pui-raise);
}
/* 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); }

/* --------------------------------------------------------------------------
   `.pui-delta--spend` — the same pill on a SPENDING surface
   --------------------------------------------------------------------------
   Markup:  <span class="pui-delta pui-delta--up pui-delta--spend">
              <svg aria-hidden="true" …/>+18.4%
            </span>

   `--up` and `--down` keep their meaning — they describe the ARROW, i.e. which
   way the number moved. `--spend` describes the SURFACE: on a spending chart
   the number moving up is the bad news, so the colours swap while the arrow,
   the class name and every other delta on the site stay exactly as they were.
   That is why this is a third class and not a redefinition of the first two;
   the spending-analysis port had to scope its own pair precisely because
   flipping --pui-up/--pui-down would have inverted the KPI row on the
   dashboard at the same time.

   Contrast (ink on its OWN tinted pill, which is the worst case):
     dark   rose #ffc266 8.44:1 · fell #65d6aa 7.63:1
     light  rose #8a4b00 6.00:1 · fell #006b4f 5.76:1

   TRAP: colour is the second channel here, never the first. The arrow glyph
   and the sign in the label carry the direction; a reader who cannot separate
   amber from green still gets "+18.4% ▲". Do not ship the pill with a bare
   number in it.
   TRAP: do NOT pair `--spend` with `.pui-delta--inline`. The inline flavour
   drops its background, and these two inks were measured on their pills.
   -------------------------------------------------------------------------- */

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

/* --------------------------------------------------------------------------
   Chips — status labels and counts
   --------------------------------------------------------------------------
   SIZING — read this before you scope a font-size on one.

   `.pui-chip` is .917em, and .917 of WHAT is the whole problem: a chip
   inherits from its parent, and if no ancestor sets a size it inherits the
   `.pui` root. Dropped into a 12px toolbar or beside a 12px meta line, an
   unscoped chip therefore renders at ~13.75px — LARGER than the text it
   annotates. Two sections found this independently and each fixed it a
   different way (one scoped the chip, one scoped the row).

   The base is deliberately NOT changed to .8em. Receipts scopes the row
   (`.pui-filerow__state { font-size: .8em }`), so a smaller base would
   multiply and land the chip at 9.6px. The three fixes are instead:

     • `--pui-chip-fs`     the knob. Set it on the chip or on any ancestor.
     • `.pui-chip--sm`     .8em. Use this in a toolbar, a meta row, or
                           anywhere the surrounding type is 12px or smaller.
     • `.pui-filerow__state` now carries its own 13px context, so a chip
                           inside an upload row is correctly sized with no
                           section-local rule at all.

   The rule of thumb: a chip should never be larger than the line it annotates.
   If it is, reach for `--sm` — do not scope its parent, because the parent may
   already be sizing something else.
   -------------------------------------------------------------------------- */

.pui-chip {
  display: inline-flex;
  align-items: center;
  gap: .35em;
  padding: .2em .6em;
  border-radius: var(--r-pill);
  font-size: var(--pui-chip-fs, .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);
}

/* 12px on the 15px app root — the size the app's own `text-xs` chip is. */
.pui-chip--sm { --pui-chip-fs: .8em; }

.pui-chip--ok    { color: var(--pui-chip-ok-ink);    background: var(--pui-info); }
.pui-chip--good  { color: var(--pui-chip-good-ink);  background: var(--pui-pos); }
.pui-chip--warn  { color: var(--pui-chip-warn-ink);  background: var(--pui-warn); }
.pui-chip--alert { color: var(--pui-chip-alert-ink); background: var(--pui-alert); }

/* --------------------------------------------------------------------------
   `.pui-chip--outline` — the bordered status badge
   --------------------------------------------------------------------------
   Markup:  <span class="pui-chip pui-chip--outline pui-chip--sm">In Progress</span>

   §6.2 promised `.pui-statusframe` as "a bordered translucent status pill" and
   what shipped under that name was the METER frame (now `.pui-meterframe`).
   This is the badge that was actually promised. The travel port needed it and
   hand-rolled it; this is that component.

   Why it is not just another `--ok`/`--warn` fill: a filled chip says "this
   thing IS that value" (Uploaded, 62%, 4 needs review). An outline badge says
   "this thing is currently in that STATE" — the product uses
   `<Badge variant="outline">` for exactly the lifecycle states a thing moves
   through. Different meaning, so a different shape, not a fifth colour.

   Colour comes entirely from --pui-badge-ink / -bg / -edge, so `.pui--light`
   re-points it like everything else. The edge is drawn with an inset
   box-shadow rather than a border on purpose: `.pui-chip` has no border, and
   adding one here would change the pill's outer size and mis-align it against
   a filled chip standing beside it in the same row.
     ink 5.25:1 dark / 5.49:1 light on the plate · edge 4.30:1 / 3.51:1
   TRAP: this is a still life like everything else in the kit. It is a <span>,
   it is not focusable, and it never becomes a filter control.
   -------------------------------------------------------------------------- */

.pui-chip--outline {
  color: var(--pui-badge-ink);
  background: var(--pui-badge-bg);
  box-shadow: inset 0 0 0 1px var(--pui-badge-edge);
}

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

.pui-tablewrap {
  border: 1px solid var(--pui-line-strong);
  border-radius: var(--pui-r);
  background: var(--pui-surface);
  box-shadow: var(--pui-raise);
  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.

   Knob:  --pui-panelkicker-gap        space to whatever follows. Default
                                       2.143em (30px on this element's 14px).

   WHY THE KNOB. The 30px default is not a rhythm value, it is a MEASURED one:
   it was set against a `.pui-rows` on the 53px Figma pitch, where 30px of air
   is what puts the first row's baseline where the file puts it. Put the kicker
   above anything else — a dropzone, a tighter `.pui-rows`, a chart — and 30px
   is simply the wrong number, which is why both bands that use this primitive
   already override `margin-bottom` by hand (16px in receipts, 22px in
   statements). Two hand-overrides of the same declaration is the signal that
   it wanted to be a knob.

   The pitch rule of thumb: if the thing below is a `.pui-rows` at the default
   pitch, leave it alone. Otherwise set the gap to the leading you actually
   want and say what you measured it against.
   -------------------------------------------------------------------------- */

.pui-panelkicker {
  margin: 0 0 var(--pui-panelkicker-gap, 2.143em);  /* 30px on its 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.
     --pui-rows-lastrule the trailing rule under the LAST row. Default
                         `1px solid var(--pui-rule)`, i.e. every row is ruled.
                         Set it to `0` when the readout is a section inside a
                         larger block that draws its own boundary underneath —
                         otherwise the two hairlines stack and read as a
                         double rule. The travel drawer hit exactly this and
                         zeroed `border-bottom` on `:last-child` by hand.
                         It is a full border shorthand, not a colour, so a
                         caller can also thicken or re-colour just the closer.

   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);
}

/* The closing rule is a separate declaration so a caller can drop it without
   also dropping the padding that sets the row's own height. */
.pui-rows__row:last-child {
  border-bottom: var(--pui-rows-lastrule, 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.
   Modifiers:
     .pui-card--flush      body has no padding — for a `.pui-table` that must
                           run edge to edge (the table draws its own gutters).
     .pui-card--attention  the amber "wants a look" block; see below.
   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.
   Trap:    the head's own bottom hairline is `--pui-line`, a rule INSIDE the
            card. It is not the card's boundary — `--pui-line-strong` is. Do
            not swap one for the other.
   -------------------------------------------------------------------------- */

.pui-card {
  --pui-card-pad: .9375em;      /* 15px                                      */
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* The kit's own example for `.pui-chart` prescribes a <figure>, and the
     spending band puts `.pui-card` on one too — which is right, a titled chart
     IS a figure. A <figure> carries a 1em UA margin-inline of 40px, so an
     unreset card renders inset from its own grid cell and misaligned against
     the card beside it. `.pui-chart` already zeroed this; the card had not,
     and the band had to hand-reset both. Zeroed here for the same reason
     `.pui-rows`, `.pui-filelist` and `.pui-legend` zero theirs: a container
     primitive owns its own outer geometry, not the element it happens to be. */
  margin: 0;
  border: 1px solid var(--pui-line-strong);
  border-radius: var(--pui-r);
  background: var(--pui-surface);
  box-shadow: var(--pui-raise);
}

.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-card--attention` — the amber "this wants a look" block
   --------------------------------------------------------------------------
   Markup:
     <section class="pui-card pui-card--attention" aria-labelledby="sug-t">
       <div class="pui-card__head">
         <h4 class="pui-card__title" id="sug-t">Suggested trip expenses (4)</h4>
         <p class="pui-card__aside">Not yet linked</p>
       </div>
       <ul class="pui-card__body pui-card--flush …"> … </ul>
     </section>

   WHAT IT MEANS, and the distinction that keeps it from becoming a warning:
   attention is "someone should look at this", not "something is wrong".
   `Suggested trip expenses (4)` is the canonical case — nothing failed, the
   app has just found four things it would like a decision on. A real error
   state is a `.pui-chip--alert`, and it is red, and it is not this.

   Four properties, because a tinted block needs all four and taking any one
   of them from the app's raw amber fails — see the --pui-attention-* note in
   the token block. Measured:
     dark   ink 7.91:1 on the wash · edge 5.79:1 on --pui-surface
     light  ink 6.44:1 on the wash · edge 3.49:1 on white
   The WASH is 1.19:1 / 1.06:1 and that is correct: it is a tint, not the
   boundary. The edge is the boundary.

   Traps:
     • The title inherits --pui-attention-ink, so it is amber. The BODY text
       does not and must not — a block of amber prose is a wall, and the
       amber's job is to point at the block, not to paint everything in it.
     • --pui-attention-rule is for dividing stacked rows INSIDE the block. It
       is not `--pui-line`; --pui-line is grey and disappears against the wash.
     • Colour never carries the state alone. The count in the title ("(4)") is
       the accessible signal; the amber is the glance-level one.
   -------------------------------------------------------------------------- */

.pui-card--attention {
  border-color: var(--pui-attention-edge);
  background: var(--pui-attention-bg);
}
.pui-card--attention > .pui-card__head { border-bottom-color: var(--pui-attention-rule); }
.pui-card--attention > .pui-card__head > .pui-card__title { color: var(--pui-attention-ink); }
/* Row divider inside the block. Works for <li>+<li> or any stacked children. */
.pui-card--attention .pui-card__body > * + * { border-top: 1px solid var(--pui-attention-rule); }

/* --------------------------------------------------------------------------
   .pui-meterframe — the bordered box that packages a meter
   --------------------------------------------------------------------------
   RENAMED, and the old name is the reason. This shipped as `.pui-statusframe`
   against a §6.2 line that described "a bordered translucent status pill" —
   but what was built was the frame around the "On Track" METER, which is a
   different object entirely. The travel port went looking for the status pill
   the name promised, found this, and had to hand-roll the badge. The badge now
   exists as `.pui-chip--outline`; this is the meter frame and is named for it.

   `.pui-statusframe` is kept as an alias so nothing breaks, but it is
   deprecated: do not write it in new markup. (No section currently uses either
   name — the rename was verified against every section file before it was
   made — so the alias is belt-and-braces for in-flight work, not a migration.)

   Markup:
     <div class="pui-meterframe">
       <p class="pui-meterframe__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.
   Trap:    do not put a `.pui-meter--progress` in here. That flavour turns its
            own track hairline on because it has no frame; inside one you get a
            box in a box. The frame is for the read-out flavour.
   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-meterframe,
.pui-statusframe {              /* deprecated alias — see above              */
  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-meterframe__label,
.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-meterframe__label svg,
.pui-statusframe__label svg {
  flex: none;
  width: 1.16667em;             /* 14px                                      */
  height: 1.16667em;
  color: var(--pui-blue-text);
}

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

/* ==========================================================================
   FINANCIAL ATMOSPHERES
   --------------------------------------------------------------------------
   `.pui-atmos` is a REGION, not a card. The product divides a page into named
   zones — executive summary, things that need you, the ledger, reserves — and
   tints each one with its own barely-there gradient
   (packages/ui-kit/src/lib/financial-atmosphere.ts). It is the app's way of
   saying "you are in a different part of the page" without drawing a box.

   Two sections render these (dashboard: exec + action; travel: ledger) and
   both transcribed the gradients by hand, which is exactly the drift this file
   exists to stop.

   Markup — the __in wrapper is REQUIRED:
     <section class="pui-atmos pui-atmos--exec" aria-labelledby="exec-t">
       <div class="pui-atmos__in">
         <h3 id="exec-t">Cash health</h3>
         …
       </div>
     </section>

   ARIA:    the atmosphere is decoration; the REGION is the content. Use a
            real <section> with aria-labelledby pointing at its own heading,
            or the zone the tint is announcing exists only visually.

   Modifiers — pick the one that matches what the zone is FOR, not the colour
   you want. That is the whole contract; the hues are the app's:
     --exec      blue from the top left    — the summary you read first
     --action    blue from the top right   — what needs a decision
     --ledger    teal from the bottom right— transactions, records, history
     --reserve   slate from the top        — set-aside, savings, buffers

   Knobs:   --pui-atmos-pad (1.25em / 20px) · --pui-atmos-r (.5em / 8px)
            --pui-atmos-edge · --pui-atmos-shadow · --pui-atmos-vignette

   THE EDGE IS A DELIBERATE DIVERGENCE from the product and the token block
   explains why: an atmosphere measures 1.09:1 against the app canvas on dark
   and 1.04:1 against white on light. Inside the app that is fine, because the
   canvas around it is itself one step off. On a marketing band there is no
   canvas and the region has no edge at all. --pui-atmos-edge points at
   --pui-line-strong: 3.16:1 dark / 3.35:1 light on the atmosphere's own
   mid-stop. A section reproducing an atmosphere that really does sit inside a
   rendered app canvas can set it to `transparent`.

   Traps:
     • Content MUST go in `.pui-atmos__in`. The vignette is a ::before at z-0
       and un-wrapped children stack under it — text goes grey at the corners.
     • Do not nest atmospheres. Two vignettes multiply and the inner region
       reads as a hole.
     • Ink on an atmosphere: --pui-text 14.9:1 dark / 16.9:1 light, --pui-muted
       6.0:1 / 5.4:1 at the worst gradient stop. The app's own
       dark:text-white/68 and /44 measure 4.3:1 and 4.2:1 there — under AA at
       12px. Use the kit tokens; see the porting table at the head of the file.
   ========================================================================== */

.pui-atmos {
  --pui-atmos-pad: 1.25em;      /* 20px — lg:p-6 at the app's own root       */
  --pui-atmos-r: .5em;          /* 8px — rounded-lg                          */
  position: relative;
  isolation: isolate;
  overflow: hidden;
  min-width: 0;
  padding: var(--pui-atmos-pad);
  border: 1px solid var(--pui-atmos-edge);
  border-radius: var(--pui-atmos-r);
  background: var(--pui-atmos-base);
  box-shadow: var(--pui-atmos-shadow);
}

/* The vignette. Sits ABOVE the background and BELOW __in. */
.pui-atmos::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: var(--pui-atmos-vignette);
  pointer-events: none;
}

.pui-atmos__in { position: relative; z-index: 1; min-width: 0; }

/* Each modifier restates the base layer under its own radial, because a
   comma-separated background cannot carry a colour in a non-final layer and
   an empty first layer is not expressible. Transcribed, not approximated. */
.pui-atmos--exec    { background: var(--pui-atmos-exec),    var(--pui-atmos-base); }
.pui-atmos--action  { background: var(--pui-atmos-action),  var(--pui-atmos-base); }
.pui-atmos--ledger  { background: var(--pui-atmos-ledger),  var(--pui-atmos-base); }
.pui-atmos--reserve { background: var(--pui-atmos-reserve), var(--pui-atmos-base); }

/* ==========================================================================
   .pui-tchart — a signed total over a two-column split
   --------------------------------------------------------------------------
   The shape the app uses wherever one net figure is the sum of two opposing
   ones: net cash flow over cash-in / cash-out, net change over
   contributions / withdrawals, variance over budget / actual. Centred label,
   signed value, a rule, then two divided cells.

   Markup:
     <div class="pui-tchart">
       <div class="pui-tchart__net">
         <p class="pui-tchart__label">Net cash flow</p>
         <p class="pui-tchart__netval pui-tchart__netval--pos">+$1,842</p>
       </div>
       <div class="pui-tchart__split">
         <div class="pui-tchart__cell">
           <p class="pui-tchart__label">Cash in</p>
           <p class="pui-tchart__val pui-tchart__val--in">$4,150</p>
         </div>
         <div class="pui-tchart__cell">
           <p class="pui-tchart__label">Cash out</p>
           <p class="pui-tchart__val">$2,308</p>
         </div>
       </div>
     </div>

   ARIA:    none of its own — it is six pieces of real text and it reads in
            order as "Net cash flow, +$1,842, Cash in, $4,150, Cash out,
            $2,308", which is correct and needs no help. Do NOT reach for a
            <table>: there is no row/column relationship here, only a total
            and its two parts. Do not aria-hide it either; unlike a chart, the
            numbers ARE the content.

   Knobs:   --pui-tchart-rule (the divider) · --pui-tchart-gap (space between
            the net block and the split, default 12px)

   Traps:
     • The SIGN is not optional on __netval. A net figure printed without one
       is ambiguous, and colour is not allowed to be the only thing carrying
       the direction — --pos and --neg are the second channel, never the first.
     • __val--in tints the money-IN cell with --pui-pos (8.88:1 dark / 5.18:1
       light at the worst atmosphere stop). The money-OUT cell is deliberately
       left as --pui-text: painting it red would read as an error, and spending
       money is not an error. Only ONE of the two cells is ever tinted.
     • The divider is the one hairline in this file not held to 3:1. It is a
       grouping affordance and both cells carry their own label; see
       --pui-tchart-rule in the token block for the reasoning.
     • It sizes off its parent, so drop it inside a `.pui-atmos__in` or a
       `.pui-card__body` and it inherits that context.
   ========================================================================== */

.pui-tchart {
  --pui-tchart-gap: .75em;      /* 12px                                      */
  min-width: 0;
}

.pui-tchart__net { text-align: center; }

.pui-tchart__label {
  margin: 0;
  font-size: .6875em;           /* 11px — the app's HERO_LABEL_CLASS         */
  font-weight: var(--fw-semibold);
  letter-spacing: .04em;
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--pui-muted);
}

.pui-tchart__netval {
  margin: .125em 0 0;
  font-size: 1em;
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
  color: var(--pui-text);
}
.pui-tchart__netval--pos { color: var(--pui-pos); }
.pui-tchart__netval--neg { color: var(--pui-down); }

.pui-tchart__split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: var(--pui-tchart-gap);
  padding-top: var(--pui-tchart-gap);
  border-top: 1px solid var(--pui-tchart-rule);
}

.pui-tchart__cell {
  min-width: 0;
  padding: 0 .75em;
  text-align: center;
}
.pui-tchart__cell + .pui-tchart__cell { border-left: 1px solid var(--pui-tchart-rule); }

.pui-tchart__val {
  margin: .375em 0 0;
  font-size: 1.5em;             /* 24px — sm:text-2xl                        */
  font-weight: var(--fw-semibold);
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  color: var(--pui-text);
}
.pui-tchart__val--in { color: var(--pui-pos); }

/* ==========================================================================
   .pui-accounttile — the AccountTile
   --------------------------------------------------------------------------
   The most distinctive object in the product, and the reason this block is
   long: nothing else in Vaulted OS looks like it. A 232×112 card split into a
   34px engraved left rail and a body — the rail carries the institution mark
   over a rotated bottom-to-top uppercase type label, the body carries a ghost
   institution monogram bleeding off the bottom-right corner, and the balance
   is an engraved figure pinned bottom-left with its symbol, thousands
   separator and cents each set apart from the integers.

   Ported from packages/ui-kit/src/components/accounts/account-tile.tsx
   (size="compact"), which the dashboard band had already rebuilt as ~200
   lines of section-local CSS. Every colour goes through a --pui-tile-*
   property so `.pui--light` re-points the whole object — which matters more
   here than anywhere else in the kit, because the tile is a LIGHT object in
   the product and its dark skin is the derivative.

   NAMING NOTE: the --pui-tile-* properties belong to THIS component.
   `.pui-tile` (the KPI tile) uses only --pui-tile-accent and --pui-tile-cols.
   The overlap is unfortunate and predates this port; do not "tidy" one into
   the other, they are different objects.

   Markup — every part, in order:
     <li class="pui-accounttile pui-accounttile--connected">
       <div class="pui-accounttile__rail">
         <span class="pui-accounttile__mark" aria-hidden="true">CH</span>
         <span class="pui-accounttile__type">Checking</span>
       </div>
       <div class="pui-accounttile__body">
         <span class="pui-accounttile__ghost" aria-hidden="true">CH</span>
         <div class="pui-accounttile__top">
           <div class="pui-accounttile__id">
             <p class="pui-accounttile__name">Chase Business Complete</p>
             <span class="pui-accounttile__mask">••4471</span>
           </div>
           <p class="pui-accounttile__health">Connected</p>
         </div>
         <p class="pui-accounttile__bal">
           <span class="pui-accounttile__cur">$</span
           ><span class="pui-accounttile__int">128</span
           ><span class="pui-accounttile__comma">,</span
           ><span class="pui-accounttile__int">402</span
           ><span class="pui-accounttile__cents">.18</span>
         </p>
       </div>
     </li>
   A negative balance adds `<span class="pui-accounttile__sign">−</span>`
   before __cur.

   ARIA:
     • The rail MARK is aria-hidden. It is the first two letters of the
       institution name, which the __name line already says in full; announced
       on its own it is noise. The rail's __type ("Checking") is NOT hidden —
       it is the only place the account type appears.
     • The GHOST is aria-hidden and always will be: it is the same monogram
       again, at 47px, as decoration.
     • __health is real text. It must be, because the health COLOUR is the
       only other channel and colour is never allowed to carry a state alone.
     • The balance is split across five spans for typography. Screen readers
       concatenate adjacent inline text, so "$128,402.18" is announced whole —
       but ONLY if there is no whitespace between the spans. Keep the
       `></span` line-break style above, or the reader hears "128 , 402".
     • The tile is a still life. It is not a <button>, not an <a>, and it does
       not get a tabindex, per the `.pui-btn` contract.

   Modifiers — one per health state, and the state names are the product's:
     --connected     a live Plaid connection
     --manual        a hand-entered account (never Plaid, so never "needs
                     attention" — see the note below)
     --attention     a Plaid connection that needs re-auth
     --disconnected  a dead connection

   THE PRODUCT FAILS AA HERE AND THIS KIT DOES NOT, which is the one place the
   two knowingly diverge. account-tile.tsx:29-34 gives each state ONE hex for
   both themes. On the LIGHT tile all four fail — Connected #34d17f is 1.72:1,
   Manual 1.85:1, Needs attention 1.46:1, Disconnected 3.14:1 — for a 9px
   extra-bold uppercase label. And on the DARK tile Disconnected #f0475f is
   4.19:1 at the body's lightest gradient stop, which is where the label sits.
   So: the light column is stepped on all four, and the dark column on one.
   Every stepped value keeps the app's hue. Measured on the worst paper stop:
     dark   connected 7.67 · manual 7.09 · attention 8.99 · disconnected 4.98
     light  connected 4.49 · manual 4.76 · attention 5.44 · disconnected 5.53
   If the product ever fixes its own tile, take these values back to it.

   Traps:
     • A `--attention` tile beside a "Healthy" summary bar is a state the
       product cannot reach (getBalanceRailHealth returns Healthy only when no
       Plaid account needs attention). A `--manual` tile can sit beside one,
       because a manual account is not Plaid. Do not mix them incorrectly in a
       shot — the state is checkable and someone will check it.
     • The mask, the currency symbol, the thousands separator and the cents
       all wear --pui-tile-meta. The app tints the separator #67748d (3.7:1)
       and the mask #65758c (4.05:1); both are stepped here. The separator
       still reads a shade below the integers, which is the point of tinting it.
     • --pui-tile-ghost-blend is `screen` on dark and `multiply` on light. It
       is a property because those are not the same operation; do not collapse
       them to one value.
     • The tile is 112px tall and the balance is absolutely positioned inside
       it. Do not let __name wrap past two lines — it is line-clamped for that
       reason, and a third line collides with the figure.
     • __ghost's `width`/`height`/`right`/`bottom` are ems of ITS OWN 47px
       font-size, not of the tile — so the box resolves to ~231px and the
       monogram lands centre-right and bleeds off two edges. That is the
       shipped appearance and it is correct; it just is not what the numbers
       look like. Do not "fix" the arithmetic.
     • The ghost is behind __top (z-0 vs z-2) but its 40%-opacity stroke does
       cross __health where the two meet. The health measurements above are on
       clean paper; over a ghost stroke they dip. Keep the health string short
       — the product's own strings are — and never lower the opacity of the
       label to compensate.
     • Do not render this tile below ~214px wide. The container query below
       drops the grid to one column at 440px for exactly that reason: at 170px
       the health label prints on top of the account name. If you place tiles
       outside `.pui-accounttiles`, you own that floor yourself.
   ========================================================================== */

/* Grid wrapper. Steps 2 → 4 and never passes through 3: the app's own
   `auto-fill minmax(227px, 1fr)` lands on three columns at most panel widths,
   which drops a fourth tile onto a row of its own beside a hole. A real app
   with nine accounts never shows that; a marketing still with exactly four
   always would. The 4-up column is ~13.4em (214px), within a hair of the
   app's 227px floor. */
.pui-accounttiles {
  display: grid;
  grid-template-columns: repeat(var(--pui-accounttile-cols, 2), minmax(0, 1fr));
  gap: .75em;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pui-accounttile {
  --pui-at-h: var(--pui-tile-h-connected);
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: stretch;
  min-width: 0;
  height: 7em;                  /* 112px                                     */
  border-radius: 1em;           /* rounded-2xl                               */
  background: var(--pui-tile-bg);
  box-shadow: var(--pui-tile-shadow);
}

/* The hairline ring the tile draws OVER everything, including the rail seam. */
.pui-accounttile::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 6;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--pui-tile-ring);
  pointer-events: none;
}

.pui-accounttile--connected    { --pui-at-h: var(--pui-tile-h-connected); }
.pui-accounttile--manual       { --pui-at-h: var(--pui-tile-h-manual); }
.pui-accounttile--attention    { --pui-at-h: var(--pui-tile-h-attention); }
.pui-accounttile--disconnected { --pui-at-h: var(--pui-tile-h-disconnected); }

.pui-accounttile__rail {
  position: relative;
  z-index: 2;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 2.125em;               /* 34px                                      */
  padding: .5625em 0 .5em;      /* pt-[9px] pb-2                             */
  border-radius: 1em 0 0 1em;
  background: var(--pui-tile-rail-bg);
  box-shadow: var(--pui-tile-rail-shadow);
}
/* The engraved shadow the rail casts into the body. */
.pui-accounttile__rail::after {
  content: '';
  position: absolute;
  top: .3125em;
  right: 0;
  bottom: .3125em;
  width: .5625em;
  border-radius: .5em 0 0 .5em;
  background: var(--pui-tile-rail-edge);
  pointer-events: none;
}

/* AccountBrandMark's monogram fallback — `initialsFromName`, the first two
   letters uppercased (account-brand-mark.tsx:33-36). The site ships NO bank
   artwork and must not imply a logo a tenant may not have cached. */
.pui-accounttile__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 1.25em;               /* h-5                                       */
  font-size: .75em;             /* text-xs                                   */
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
  color: var(--pui-tile-mark);
  text-shadow: var(--pui-tile-mark-shadow);
}

.pui-accounttile__type {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  writing-mode: vertical-rl;
  rotate: 180deg;
  font-size: .625em;            /* 10px                                      */
  font-weight: var(--fw-semibold);
  letter-spacing: .05em;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--pui-tile-type);
}

.pui-accounttile__body {
  position: relative;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  padding: .625em .625em 0 .8125em;   /* pt-2.5 pr-2.5 pl-[13px]             */
  border-radius: 0 1em 1em 0;
  background: var(--pui-tile-body-bg);
}
/* The light-catch down the body's leading edge, where the rail meets it. */
.pui-accounttile__body::before {
  content: '';
  position: absolute;
  top: .3125em;
  bottom: .3125em;
  left: 0;
  z-index: 3;
  width: 1px;
  background: var(--pui-tile-body-edge);
}

/* Ghost watermark, bleeding off the corner (account-tile.tsx:394-421). With no
   logo the monogram is drawn as an outline: transparent fill, hairline stroke,
   40% opacity, blended into the paper. */
.pui-accounttile__ghost {
  position: absolute;
  right: -.8125em;
  bottom: -.8125em;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 5.25em;
  height: 5.25em;
  font-size: 2.9375em;          /* 47px                                      */
  font-weight: 800;
  letter-spacing: -.05em;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: .038em var(--pui-tile-ghost-stroke);
  opacity: .4;
  mix-blend-mode: var(--pui-tile-ghost-blend);
  filter: var(--pui-tile-ghost-filter);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Institution logos on the tile
   --------------------------------------------------------------------------
   The rules above draw the MONOGRAM, which in the product is the fallback, not
   the default: `AccountBrandMark` renders the cached institution logo whenever
   one exists and only derives letters when it does not. The logo pipeline has
   since shipped, so a marketing tile drawing "CH" is showing a state a real
   connected account no longer reaches.

   Drop an <img> inside either slot and it takes over. Nothing else changes —
   the rail geometry, the ghost's blend mode and its bleed off the corner are
   the monogram's, and the logo inherits them.

   TWO MEASURED KNOBS, and they are per-institution, not per-tile:

     --pui-tile-mark-scale   the rail mark's size multiplier
     --pui-tile-ghost-alpha  the ghost's opacity

   The product derives both from the logo's measured ink density
   (`account-tile-ink.ts`): a dense wordmark like American Express would print
   as a dark slab at the monogram's weight, so it is drawn smaller and fainter
   than a light glyph like the Chase octagon. The published pairs are
   chase [0.3, 1.0] and american-express [0.19, 0.88] — ghost alpha first, rail
   scale second — against a [0.3, 1.0] reference. Do not eyeball replacements;
   the whole point of the table is that equal-looking logos are not equal ink.

   The ghost drops the text stroke — there is no glyph to stroke — but KEEPS
   both `mix-blend-mode` and the saturate/brightness knock-back. Those are on
   the ghost's container in the product and apply to a logo exactly as they do
   to a monogram; removing them for logos was tried here and it prints the Amex
   card as a hard blue slab across the balance. */

.pui-accounttile__mark img {
  display: block;
  width: calc(1.25em * var(--pui-tile-mark-scale, 1));
  height: auto;
}

/* THE GHOST TAKES THE COLOUR LOGO, like the rail — this is what the product
   does, and the brand colour is the whole point of the logo pipeline.

   It is knocked back by three things at once, all inherited from the base rule
   and all measured rather than chosen: the per-institution alpha, the tile's
   `mix-blend-mode` (multiply on light paper, screen on dark), and the
   saturate/brightness filter. An Amex card and a Chase octagon carry very
   different ink, which is exactly why the alpha is per-institution.

   HISTORY, so nobody re-fixes this the wrong way: this was briefly masked to a
   flat `-mono` silhouette because the Amex wordmark printed legibly across the
   balance. That was real, but the cause was a sizing bug — the base sets
   `font-size: 2.9375em` to place a 47px monogram, and `width: 5.25em` compounds
   off it, so the ghost box rendered at 177px instead of 84. Centred text does
   not care; a logo filling its box does. Reset the font-size and the colour
   logo sits correctly. */
.pui-accounttile__ghost--logo {
  -webkit-text-stroke: 0;
  opacity: var(--pui-tile-ghost-alpha, .3);
  /* NO saturate/brightness knock-back on a logo. The base applies
     `saturate(.6) brightness(.94)`, which flatters a stroked monogram but
     drains a brand mark: measured against a ghost-free render, the filtered
     mark deposits MORE ink than the unfiltered one (700 vs 657) while reading
     grey-lilac instead of Chase blue. It is heavier AND less branded — there is
     no tradeoff to weigh, so it goes. The per-institution alpha and the tile's
     mix-blend-mode still do the knocking back. */
  filter: none;
  /* See above: back to the tile's own em so 5.25em is 5.25/7 of tile height,
     which is the product's 84/112. The -.8125em offsets come right for free. */
  font-size: 1em;
}
.pui-accounttile__ghost--logo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.pui-accounttile__top {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .625em;
}

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

.pui-accounttile__name {
  margin: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  font-size: .71875em;          /* 11.5px                                    */
  font-weight: var(--fw-semibold);
  letter-spacing: -.005em;
  line-height: 1.24;
  color: var(--pui-tile-name);  /* 17.3:1 dark · 15.5:1 light                */
}

.pui-accounttile__mask {
  display: block;
  margin-top: .1875em;
  font-size: .59375em;          /* 9.5px                                     */
  font-weight: var(--fw-semibold);
  letter-spacing: .06em;
  line-height: 1.2;
  color: var(--pui-tile-meta);  /* 6.2:1 dark · 4.6:1 light                  */
}

.pui-accounttile__health {
  flex: none;
  margin: 0;
  max-width: 48%;
  overflow: hidden;
  padding-top: .125em;
  font-size: .5625em;           /* 9px                                       */
  font-weight: 800;
  letter-spacing: .08em;
  line-height: 1.2;
  text-transform: uppercase;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--pui-at-h);
}

/* The engraved balance. In-app this is two stacked BalanceFigures — a plate
   knockout behind a stroked figure. ONE layer is drawn here: at shot scale the
   knockout is sub-pixel and only muddies the glyph edges. */
.pui-accounttile__bal {
  position: absolute;
  right: .625em;
  bottom: .5625em;
  left: .8125em;
  z-index: 2;
  margin: 0;
  display: flex;
  align-items: baseline;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--pui-tile-figure);
}
.pui-accounttile__sign {
  margin-right: .125em;
  translate: 0 -.20em;
  font-size: .9625em;
  font-weight: 800;
}
.pui-accounttile__cur {
  align-self: flex-start;
  margin: .10em .125em 0 0;
  font-size: .6125em;
  font-weight: var(--fw-bold);
  color: var(--pui-tile-meta);
}
.pui-accounttile__int {
  font-size: 1.75em;            /* the app's --q-fs, capped at 28px          */
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: .92;
  -webkit-text-stroke: max(.4px, .022em) var(--pui-tile-figure-stroke);
  paint-order: stroke fill;
}
.pui-accounttile__comma { color: var(--pui-tile-meta); }
.pui-accounttile__cents {
  margin-left: .125em;
  translate: 0 -.06em;
  font-size: .7275em;
  font-weight: var(--fw-bold);
  color: var(--pui-tile-meta);
}

/* ==========================================================================
   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.
   ========================================================================== */

/* THE EDGE. This was `--pui-stroke-strong` and `--pui-chip-bg`, both of which
   are calibrated as decorative inner seams inside a card:
     stroke-strong .10 white  1.24:1 on --bg-deep · 1.31:1 on --pui-surface
     chip-bg       .08 white  1.17:1 on --bg-deep
   i.e. on a bare marketing band the default button had no boundary at all and
   read as a floating word. It is a UI COMPONENT — a picture of one, but the
   reader still has to see where it is — so its edge is held to 3:1 wherever it
   lands, via --pui-btn-edge:
     3.23:1 on --bg-deep · 3.27:1 on --bg-deep-2 · 3.33:1 on --pui-surface
     3.12:1 on white (light column)
   The fill goes through --pui-btn-bg for the same reason, which is what lets
   `.pui--bare` drop the plate and keep the outline (a 3:1 plate behind a 3:1
   outline is two boundaries doing one job). */
.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-btn-edge);
  border-radius: .75em;         /* 12px — matches --r-md                     */
  background: var(--pui-btn-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                                      */
}

/* --------------------------------------------------------------------------
   `.pui-btn--icon` — the icon-only still life
   --------------------------------------------------------------------------
   Markup — FOUR parts, and the third is the one that gets forgotten:
     <span class="pui-btn pui-btn--icon pui-btn--ghost" aria-hidden="true">
       <svg aria-hidden="true" …/>
     </span>

     • <span>, aria-hidden, no tabindex     — the `.pui-btn` contract, verbatim
     • --icon                               — square box, no label padding
     • --ghost (or --primary)               — pick a skin; see the trap below
     • the SVG is aria-hidden too           — belt and braces, and it stops a
       titled icon leaking a name out of an aria-hidden subtree in some engines

   The product draws a lot of these (`variant="ghost" size="icon"`: the drawer's
   edit/delete/link actions, the calendar chevrons). The travel port needed
   three of them and hand-rolled `.tv-ico`.

   TRAP — the one that matters. A real icon button needs an accessible name and
   this one deliberately has none, because it is a picture. That means the icon
   is carrying meaning nowhere else on the page. Whatever the icon SAYS
   ("delete this expense", "link to a transaction") has to appear in the
   section's own figcaption or body copy. An icon still life with no caption is
   a control a screen-reader user is never told about.

   TRAP — do not pair it with --sm and then shrink the glyph as well. The box
   is 36px (30px with --sm) because that is the app's own hit target, and a
   marketing shot that draws it smaller stops looking like the product.

   Colour: the default inherits --pui-text. Two semantic tints are provided
   because the app uses exactly two — destructive and confirm — and a section
   reaching for a raw hex is how the palette forks.
   -------------------------------------------------------------------------- */

.pui-btn--icon {
  gap: 0;
  width: 2.25em;                /* 36px — square, matches the base height    */
  min-width: 2.25em;
  padding: 0;
  border-radius: .5625em;       /* 9px — a square wants a tighter radius     */
}
.pui-btn--icon.pui-btn--sm {
  width: 1.875em;               /* 30px                                      */
  min-width: 1.875em;
}
/* As an icon stroke on the surface it sits on:
     danger  5.95:1 dark · 6.78:1 light      confirm  9.33:1 dark · 5.74:1 light
   Well over the 3:1 an icon needs, and that is on purpose — a 14px glyph with
   no label beside it is the hardest thing in the kit to see. */
.pui-btn--icon.pui-btn--danger  { color: var(--pui-down); }
.pui-btn--icon.pui-btn--confirm { color: var(--pui-pos); }

/* ==========================================================================
   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;
  /* The hairline is not decoration: --pui-surface-2 sits one step off the
     card on BOTH bands, so an empty thumb (no image yet) is a rectangle you
     cannot see without it. */
  border: 1px solid var(--pui-stroke);
  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; }

/* The state slot carries its own 13px context. Without it a `.pui-chip` in
   here inherits the `.pui` root and renders ~13.75px — bigger than the 14px
   filename it annotates and noticeably bigger than the 12px meta line under
   it. See the sizing note on `.pui-chip`. A caller that wants a different
   size sets --pui-chip-fs on the chip rather than re-sizing this slot, which
   also positions the gap. */
.pui-filerow__state {
  display: flex;
  align-items: center;
  gap: .5em;
  justify-self: end;
  font-size: .8125em;           /* 13px                                      */
}

/* ==========================================================================
   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.6667em;              /* 24px ÷ 9px                                */
  height: 2.6667em;
  font-size: .5625em;           /* 9px — same 37.5% initials-to-circle ratio
                                   the other two sizes use                   */
}
.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); }

/* NOT a seventh slot. `.pui-cat-other` paints the folded tail (palette rule 2)
   and `.pui-cat-trend` paints the total/trend line drawn OVER a categorical
   chart. Both are achromatic on purpose — that is the only way neither can be
   read as an identity — and both are documented in the token block.
     other  4.95:1 dark / 5.48:1 light on the surface; 3.08 / 3.41 on a track
     trend  12.35:1 dark / 10.46:1 light on the surface
   They are named, not numbered, so `.pui-cat-6` can never be confused with
   "the last one" — slot 6 is a real category (teal) and this is not. */
.pui-cat-other { --pui-mark: var(--pui-mark-neutral); }
.pui-cat-trend { --pui-mark: var(--pui-mark-trend); }

/* --------------------------------------------------------------------------
   .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);
}

/* STANDALONE USE IS SUPPORTED, and it is the common case: `__track` + `__fill`
   are the kit's generic "one proportion in a bar", and a section reaching for
   them inside its own row layout (a comparison row, a category list) does not
   want the three-column `.pui-bar` grid around them. Every use of these two on
   the site today is standalone.
   That only works if --pui-bar-h has a FALLBACK here rather than living on
   `.pui-bar`: outside a `.pui-bar` the property is unset, `height: var(...)`
   with no fallback is an invalid value, and the track collapses to 0. One band
   discovered this and re-declared --pui-bar-h on its own track element.
   The fallback is written on the two properties that read the value, NOT as a
   `--pui-bar-h:` declaration on `__track` — declaring it here would make the
   track's own value beat an inherited one and break the knob on `.pui-bar`. */
.pui-bar__track {
  position: relative;
  height: var(--pui-bar-h, .625em);              /* 10px                     */
  border-radius: calc(var(--pui-bar-h, .625em) / 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, .625em);
  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 — the ring and the centre figure are SIBLINGS, not parent/child:
     <div class="pui-donutwrap">
       <div class="pui-donut" aria-hidden="true" style="
            --pui-donut-stops:
              var(--pui-cat-1)    0deg     147.6deg,
              var(--pui-mark-gap) 147.6deg 149.6deg,
              var(--pui-cat-2)    149.6deg 232.0deg,
              var(--pui-mark-gap) 232.0deg 234.0deg,
              … var(--pui-cat-4) 358.0deg 360deg"></div>
       <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 only. The centre figure is real text and
            is usually the section's hero number too, so it stays exposed —
            it reads as "$48,210, Last 30 days", which is exactly right. The
            slice values themselves belong in the `.pui-legend` beside it.
   Knobs:   --pui-donut-stops (the conic stop list — you write it)
            --pui-donut-size (default 11em / 176px) · --pui-donut-thickness
            (default 14%, i.e. the ring is 14% of the diameter ≈ 25px)
   Traps:
     • THE CENTRE TEXT MUST BE A SIBLING. A CSS mask clips descendants, so a
       __center nested inside .pui-donut is invisible — `mask:none` on the
       child does not escape it. That is what `.pui-donutwrap` is for.
     • 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.
       --pui-mark-gap is --pui-surface, so on a band that is NOT the kit
       surface (a `.pui--bare` readout sitting straight on a section
       background) re-point it or the gaps paint the wrong colour. Same
       hazard, same fix, for --pui-meter-ring / --pui-avatar-ring /
       --pui-spark-ring.
     • Stops are degrees and the list must run 0 → 360 with no holes, or the
       browser interpolates a smooth gradient where you wanted a hard edge.
     • Past ~5 slices a donut stops being readable. Use `.pui-bars`.
   -------------------------------------------------------------------------- */

.pui-donutwrap {
  position: relative;
  display: inline-grid;
  place-items: center;
  max-width: 100%;
}

.pui-donut {
  --pui-donut-size: 11em;       /* 176px                                     */
  --pui-donut-thickness: 14%;
  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) 0deg 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));
}

.pui-donut__center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .1em;
  text-align: center;
  pointer-events: 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);
}

/* --------------------------------------------------------------------------
   .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)

   TWO SWATCH SHAPES, and the shape is information:
     .pui-legend__swatch  a 10px block — an AREA mark. Bars, stacked bars,
                          donut arcs, the filled part of a sparkline.
     .pui-legend__line    a 16×2px rule — a STROKE mark. A trend line, a
                          target line, an average line.
   Promised last round and only __swatch shipped, so the spending band drew its
   `Total trend` key by hand. Use the one that matches how the mark is actually
   painted: a block beside a line series tells the reader to look for a filled
   region that is not there. Both read `--pui-mark`, so `.pui-cat-N` /
   `.pui-cat-trend` on the __item colours either.

   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.
     • __line defaults to --pui-mark-trend rather than slot 1. A bare
       `<span class="pui-legend__line">` is almost always the trend line, and
       defaulting it to the categorical blue is how a structural stroke ends
       up wearing a category's colour.
   -------------------------------------------------------------------------- */

.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));
}

/* Stroke key. 16 × 2px, pill-capped so it reads as a drawn line rather than a
   very thin block, and it occupies the same 10px optical column as __swatch so
   a legend mixing the two still scans down its left edge. */
.pui-legend__line {
  flex: none;
  width: 1em;                   /* 16px                                      */
  height: 2px;
  border-radius: var(--r-pill);
  background: var(--pui-mark, var(--pui-mark-trend));
}

.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) {
  /* KNOBS, not `padding`/`gap` directly — a caller that set --pui-app-pad for
     its own surface should keep winning at every width, and it cannot if this
     rule overwrites the resolved property. The values are the re-calibrated
     defaults tightened one step, not the old Figma-dashboard numbers. */
  .pui-app { --pui-app-pad: .8em; --pui-app-gap: 1em; }
  .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; }
}
/* The account rail goes four-up once the panel can afford ~214px a tile. It
   steps 2 → 4 and never through 3; see the note on `.pui-accounttiles`. */
@container (min-width: 58em) {
  .pui-accounttiles { --pui-accounttile-cols: 4; }
}

/* …and it drops to ONE below 440px, which the harness caught: two tiles in a
   380px panel are ~170px each, and at that width `.pui-accounttile__health`
   (capped at 48% of the tile) lands on top of `__name` — "Chase Busines…" and
   "CONNECT…" print over each other, with the ghost monogram behind both. The
   tile is drawn at 232px and stops working below about 214px, so 2 × 214 plus
   the 12px gap is the floor for keeping two of them side by side. */
@container (max-width: 440px) {
  .pui-accounttiles { --pui-accounttile-cols: 1; }
}

/* 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; }
}

/* ---- Round-two primitives ------------------------------------------------
   Same principle: these respond to the PANEL's width, so a product shot that
   is 420px wide in a two-column band reflows even though the viewport is
   1440. Every threshold below is the width at which a real string stops
   fitting, not a device size. */

/* A bar row is label | bar | value. Below ~420px the label column is eating
   the bar, so the label moves above it and the bar gets the full width. */
@container (max-width: 420px) {
  .pui-bar {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: .3em;
  }
  .pui-bar__label { grid-column: 1; }
  .pui-bar__value { grid-column: 2; grid-row: 1; }
  .pui-bar__track { grid-column: 1 / -1; }

  /* Keep the legend WRAPPING here — six items stacked one per line doubles
     the height of the chart, and two-up still reads. It only goes to a
     single column at 300px, below. */
  .pui-legend { gap: .5em .875em; }

  .pui-card { --pui-card-pad: .75em; }
  .pui-panel { --pui-panel-pad: 1em; --pui-panel-r: 1.125em; }
  .pui-atmos { --pui-atmos-pad: .875em; }
  .pui-dropzone { padding: 1.5em 1em; }
  .pui-dropzone__icon { width: 1.75em; height: 1.75em; }
  .pui-empty { padding: 1.5em 1em; }

  /* A 24px figure in a half-width cell is the first thing to overflow. Give
     the gutters back before shrinking the number — the number is the content. */
  .pui-tchart__cell { padding: 0 .35em; }
  .pui-tchart__val { font-size: 1.25em; }
}

/* Below ~300px the file row's thumbnail is competing with the filename. Drop
   it: the name and its progress bar are the content. */
@container (max-width: 300px) {
  .pui-filerow { grid-template-columns: minmax(0, 1fr) auto; }
  .pui-filerow__thumb { display: none; }
  .pui-legend { flex-direction: column; gap: .5em; }
  .pui-donut { --pui-donut-size: 8em; }
  .pui-donut__value { font-size: 1.0625em; }
}

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