/*
 * Prism Common Styles
 * Shared CSS for all Prism pages with dark/light mode support
 */

/* ===== TYPEFACES =====
   Self-hosted, and the reason is `size-adjust` rather than privacy or even the
   handshake. You cannot set a metric override on a Google Fonts <link>, and
   metric overrides are the single thing that turns "68 templates reflow" into
   "nothing reflows" — see --prism-font-body below.

   The handshake is the second argument and still a real one: black-dashboard's
   Poppins <link> put fonts.googleapis.com AND fonts.gstatic.com on the
   render-blocking path of every page, two DNS lookups and two TLS handshakes
   before first paint. This removes both rather than adding a third.

   Both families are VARIABLE fonts, so one file each covers every weight the
   app uses — 27 KB and 22 KB, latin subset, against six separate static
   Poppins weights before. The unicode-range is Google's own `latin` block;
   anything outside it falls through the stack to the system sans, which is the
   same behaviour the previous <link> had.

   /home loads these too rather than keeping its own Google <link>, so the
   landing page and the app cannot end up on two copies of the same typeface. */
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    /* Pins the rendered x-height to what Poppins was producing, so switching
       the body face does not grow every line box in the app's dense tables.
       See the BODY TYPE section for why this is here and not at the call
       sites, and why it is scaffolding rather than a permanent value. */
    size-adjust: 97%;
    src: url('../fonts/plus-jakarta-sans-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                   U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                   U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                   U+FFFD;
}

/* Poppins, kept for the two pieces of type that were always in it and were
   never really part of the typeface change: the big page heading, and the
   company name in the navbar. Both inherited it from `body` when Poppins was
   the app's face, so neither declared a family, and both got swept onto Space
   Grotesk as a side effect — the most prominent type on every page and the
   brand itself.

   Poppins is a static family, so each weight is its own file. Two are loaded,
   which is exactly the two in use: 700 for the wordmark, 800 for .prism-title.
   7.8 KB each, latin subset. Anything asking for a third weight is a mistake to
   catch, not a file to add. */
@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../fonts/poppins-700-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                   U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                   U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                   U+FFFD;
}

@font-face {
    font-family: 'Poppins';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('../fonts/poppins-800-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                   U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                   U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                   U+FFFD;
}

@font-face {
    font-family: 'Space Grotesk';
    font-style: normal;
    font-weight: 500 700;
    font-display: swap;
    src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                   U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                   U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                   U+FFFD;
}

/* ===== BRAND ACCENT THEME VARIABLES =====
   The accent palette is driven by CSS custom properties so users can pick a
   "skin" (see body[data-skin="…"] blocks below). Both hex and RGB-triplet
   forms are kept: hex for solid `color`/`background` uses, the RGB triplet for
   `rgba(var(--…), <alpha>)` call sites so the per-rule opacity stays inline.
   Semantic colors (success #00f2c3, warning #ff8d72, danger #fd5d93) are NOT
   vari-ised — they carry meaning and must stay constant across skins.

   :root carries the DEFAULT skin, which is `prism` — the palette the /home
   landing page is built on: blue top-left, magenta top-right, violet at the
   bottom, matching its three light leaks through the app's ambient mesh
   (accent -> accent2 -> accent3, in that anchor order). It used to carry
   `blue`, which differs in exactly one value: accent3 was #ba54f5, and is now
   #8b5cf6.

   That is not a rounding error, it is the whole point — #8b5cf6 is the violet
   in the landing page's --px-brand, so --prism-brand below reproduces its mark
   exactly. `blue` survives as its own skin with its original values rather
   than being retuned into this one, so a plain blue accent is still something
   a user can choose.

   :root holding the default (rather than whichever skin happens to be
   historically first) is what makes an element that never sees body[data-skin]
   render the brand instead of a legacy palette. */
:root {
    --prism-accent: #1d8cf8;            --prism-accent-rgb: 29, 140, 248;
    --prism-accent-light: #4da3ff;      --prism-accent-light-rgb: 77, 163, 255;
    --prism-accent2: #e14eca;           --prism-accent2-rgb: 225, 78, 202;
    --prism-accent3: #8b5cf6;           --prism-accent3-rgb: 139, 92, 246;

    /* Which appearance the BROWSER paints native widgets with — the popup a
       <select> opens, which no `option {}` rule can reach on macOS. Read by
       .prism-select / .perf-tool-select as `color-scheme: var(...)`, and
       flipped by body.white-content like every other token; without it every
       select in the app opened an OS-light menu over a dark page. */
    --prism-color-scheme: dark;

    /* Right-sidebar width. Targets 30vw, floored at 400px so the dense
       chart/table content never crushes, and hard-capped at 900px. Defined on
       :root so the sidebar AND the .main-panel padding-right (a separate DOM
       subtree) can both read it.

       It was 25vw/600px, and the share is not what was wrong with that -- 25vw
       only CLEARS the 400px floor above 1600px, so every laptop below that sat
       at the fallback that exists to stop small screens crushing, presented as
       the design. tests/capture_screenshots.py already worked around it by
       photographing the drawer at 2560px rather than at the geometry every
       other shot uses.

       --rsb-pref is the user's dragged width, written by right_sidebar.js. It
       is a SEPARATE token and the JS must never write --rsb-w directly: an
       inline custom property on :root outranks a stylesheet rule, so it would
       beat `@media (max-width: 768px) { :root { --rsb-w: 100% } }` below and
       leave a phone with a 500px drawer and a 100%-wide off-screen rest
       position. Writing the preference one level in means the phone block,
       which redefines --rsb-w itself, simply never reads it.

       900px is the hard ceiling AND the drag ceiling -- one number, not two, so
       a corrupt stored value cannot produce an absurd drawer. The default only
       reaches it at a 3000px viewport, where 900px is still 30%. */
    --rsb-w: clamp(400px, var(--rsb-pref, 30vw), 900px);

    /* Fallback only, for the instant before prism-scroll-progress.js's first
       measurement lands (see .sidebar-header and .prism-scroll-progress) --
       the navbar has no declared height, so this is not the real value on
       any given page, just a reasonable guess until JS overwrites it. */
    --prism-navbar-h: 64px;

    /* The landing page's pairing: a display face with real character for
       headings and numerals, a quiet one for prose. The app was on Poppins for
       both, which is the single largest reason it did not read as the same
       product as /home even once the colours matched.

       Poppins stays in each stack as the first fallback, deliberately: if a
       woff2 fails to load the app degrades to exactly what it looks like today
       rather than to Arial.

       Six rules in this file already asked for "Space Grotesk" or "DM Sans"
       and have been silently falling back to system sans this whole time,
       because neither font was ever loaded. They point here now. */
    --prism-font-display: 'Space Grotesk', 'Poppins', -apple-system,
                          BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --prism-font-body: 'Plus Jakarta Sans', 'Poppins', -apple-system,
                       BlinkMacSystemFont, 'Segoe UI', sans-serif;
    /* The app's original face. Two consumers, both of which were on it before
       the typeface change and neither of which declared it: .prism-title and
       the navbar wordmark. See the @font-face note above. */
    --prism-font-title: 'Poppins', 'Space Grotesk', -apple-system,
                        BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* Skin overrides — body[data-skin] (specificity 0-1-1) beats :root (0-0-1).
   `prism` is the :root default above and needs no block here;
   tests/test_theme_skins.py knows that and requires a block for every OTHER
   skin, in both directions. */

/* Monochrome blue — and it had to become one to be worth offering.
   
   It used to be the old :root palette kept intact, which meant it shared
   `prism`'s accent (#1d8cf8) exactly and differed only in accent3. Since the
   accent drives the buttons, the active nav item, focus, hover and the first
   mesh lobe, the two skins produced almost the same screen: picking between
   them changed the bottom mesh lobe and one stop of the brand gradient. Two
   adjacent swatches, one of them the product default, advertising a difference
   that was not there.
   
   So accent2 and accent3 come into the blue family. #3358f4 is
   black-dashboard's own deep blue, already in this palette's vocabulary rather
   than invented for the occasion. Now `prism` means the spectrum and `blue`
   means one hue: the mesh goes blue corner to corner, --prism-brand becomes a
   blue-to-blue ramp, and the chart series palette shifts with it. The swatches
   differ because the skins do. */
body[data-skin="blue"] {
    --prism-accent: #1d8cf8;            --prism-accent-rgb: 29, 140, 248;
    --prism-accent-light: #4da3ff;      --prism-accent-light-rgb: 77, 163, 255;
    --prism-accent2: #3358f4;           --prism-accent2-rgb: 51, 88, 244;
    --prism-accent3: #0b7ad4;           --prism-accent3-rgb: 11, 122, 212;
}

body[data-skin="purple"] {
    --prism-accent: #ba54f5;            --prism-accent-rgb: 186, 84, 245;
    --prism-accent-light: #d68bff;      --prism-accent-light-rgb: 214, 139, 255;
    --prism-accent2: #e14eca;           --prism-accent2-rgb: 225, 78, 202;
    --prism-accent3: #8965e0;           --prism-accent3-rgb: 137, 101, 224;
}

body[data-skin="green"] {
    --prism-accent: #00bf9a;            --prism-accent-rgb: 0, 191, 154;
    --prism-accent-light: #00f2c3;      --prism-accent-light-rgb: 0, 242, 195;
    --prism-accent2: #11cdef;           --prism-accent2-rgb: 17, 205, 239;
    --prism-accent3: #2dce89;           --prism-accent3-rgb: 45, 206, 137;
}

body[data-skin="orange"] {
    --prism-accent: #f5703a;            --prism-accent-rgb: 245, 112, 58;
    --prism-accent-light: #ff9460;      --prism-accent-light-rgb: 255, 148, 96;
    --prism-accent2: #fb6340;           --prism-accent2-rgb: 251, 99, 64;
    --prism-accent3: #ffa534;           --prism-accent3-rgb: 255, 165, 52;
}

body[data-skin="red"] {
    --prism-accent: #f5365c;            --prism-accent-rgb: 245, 54, 92;
    --prism-accent-light: #ff6b87;      --prism-accent-light-rgb: 255, 107, 135;
    --prism-accent2: #ec250d;           --prism-accent2-rgb: 236, 37, 13;
    --prism-accent3: #ff5e7d;           --prism-accent3-rgb: 255, 94, 125;
}

body[data-skin="pink"] {
    --prism-accent: #ec407a;            --prism-accent-rgb: 236, 64, 122;
    --prism-accent-light: #ff77a9;      --prism-accent-light-rgb: 255, 119, 169;
    --prism-accent2: #e14eca;           --prism-accent2-rgb: 225, 78, 202;
    --prism-accent3: #f06292;           --prism-accent3-rgb: 240, 98, 146;
}
body[data-skin="indigo"] {
    --prism-accent: #5e72e4;            --prism-accent-rgb: 94, 114, 228;
    --prism-accent-light: #7b8ef7;      --prism-accent-light-rgb: 123, 142, 247;
    --prism-accent2: #825ee4;           --prism-accent2-rgb: 130, 94, 228;
    --prism-accent3: #4e61d8;           --prism-accent3-rgb: 78, 97, 216;
}
body[data-skin="teal"] {
    --prism-accent: #11cdef;            --prism-accent-rgb: 17, 205, 239;
    --prism-accent-light: #45e1f7;      --prism-accent-light-rgb: 69, 225, 247;
    --prism-accent2: #00f2c3;           --prism-accent2-rgb: 0, 242, 195;
    --prism-accent3: #1be7ff;           --prism-accent3-rgb: 27, 231, 255;
}
body[data-skin="amber"] {
    --prism-accent: #f59e0b;            --prism-accent-rgb: 245, 158, 11;
    --prism-accent-light: #fbbf24;      --prism-accent-light-rgb: 251, 191, 36;
    --prism-accent2: #e67e22;           --prism-accent2-rgb: 230, 126, 34;
    --prism-accent3: #fcd34d;           --prism-accent3-rgb: 252, 211, 77;
}

/* ===== SEMANTIC SURFACE / TEXT / BORDER TOKENS =====
   The accent block above proved the pattern; this extends it past accents to
   everything else. Dark is the default (bare <body>); light is
   body.white-content.

   THE RULE: components read these tokens, never mode-specific literals. A
   `.white-content X { }` override that only restates a colour is a bug — it
   means X is reading a literal it shouldn't be. Light mode is a token flip,
   not a parallel stylesheet. tests/test_light_mode.py enforces this.

   Light values are the "Deep muted" palette: a page ground at 72% luminance
   rather than the old #f5f6fa (95%), with cards ABOVE the page and popovers
   above cards, so surfaces actually stack. --prism-text-muted is #59616f and
   not a lighter grey because anything lighter drops below WCAG AA 4.5:1
   against this dimmer ground (#727c90 measured 4.10:1 on the page). */
:root {
    /* Surfaces, in stacking order: page < card < raised.
       --prism-surface-2 is the inset/working panel; --prism-surface-hover is
       its hover state. Named by ROLE, not by number: "2 vs 3" invites getting
       the hover direction backwards, and the correct direction is opposite in
       the two modes (dark hover gets lighter, light hover gets darker). */
    /* No flat `--prism-surface` tier: cards are glass in both modes, so the
       colour behind card text is the ambient ground, not a token. One existed
       and every consumer has moved to --prism-card-bg. */
    --prism-surface-2:       rgba(255, 255, 255, 0.03);
    --prism-surface-hover:   rgba(255, 255, 255, 0.06);
    --prism-surface-raised:  #1a1d2e;

    /* Backing painted *under* .prism-card's accent wash. Cards are glass in
       BOTH modes so the ambient body::before mesh and grid read through them —
       that texture is the brand, and hiding it behind an opaque panel in one
       mode made the two themes feel like different products.

       Dark can be fully transparent because the accent wash ADDS light over a
       near-black ground, so the card still reads as raised. Light cannot: over
       a light ground a card only lifts if the veil is lighter than the ground
       under it, so light gets a deliberate alpha rather than `transparent`.
       See the light block for the number and why. */
    --prism-card-bg:         transparent;

    /* Chrome for panels that float above the page: search modal, dropdown
       menus, hover cards. These were the surfaces hardcoding #ffffff in light
       mode — the single biggest source of glare, because they are exactly the
       things that appear on top of everything else.

       Deliberately opaque: they float over arbitrary CONTENT, so glass would
       let whatever is underneath bleed through and make them unreadable. The
       right drawer used to be on this token and is not any more — it floats
       its own opaque ground and its own texture, which is a different job.
       See --prism-drawer-ground. */
    --prism-panel:           linear-gradient(180deg, #1a1d2e 0%, #151823 100%);
    --prism-overlay:         rgba(20, 30, 48, 0.98);

    /* The right drawer's own surface.

       The drawer does NOT reuse the page's material. Earlier passes tried
       both extremes and both were wrong: an opaque --prism-panel slab read as
       pre-Prism chrome pasted onto the ambient mesh, and a translucent glass
       drawer composited back to within ~3 RGB points of that same slab, so
       the change was invisible. Repeating the page's mesh + grid was wrong in
       the other direction — the drawer stopped reading as its own surface.

       So it gets its own vocabulary in the same family: a dot matrix where the
       page uses a line grid, lit by a single bloom at the drawer's own
       top-right corner rather than the page's three-point mesh.

       --prism-drawer-ground is opaque on purpose. Nothing shows through, so
       there is no backdrop-filter at any width — which is also what lets the
       phone-width drawer look identical to the desktop one instead of opting
       out. Dark sits just above --prism-bg-base so the drawer reads as raised;
       light sits above the page ground for the same reason, and below
       --prism-surface-raised so a dropdown opened inside it still separates.

       The bloom is an ALPHA, not a colour, so one geometry serves both modes:
       the gradient reads --prism-accent, which follows all nine skins. It is
       deliberately tight (85% x 26%, anchored 100% 0%) so the bright region
       sits above the content — a wider bloom put --prism-text-muted at 4.33:1
       over the labels. Light runs less than half of dark's: the same alpha
       that reads as a light source on near-black reads as a lavender wash on
       a light ground. */
    --prism-drawer-ground:   #0d0f1a;
    --prism-drawer-bloom:    0.34;
    --prism-drawer-dot:      rgba(255, 255, 255, 0.085);

    /* The navbar's glass. A token rather than a literal in the rule, because
       tests/test_light_mode_css.py has to know the band the scroll bar's track
       sits on -- the gradient's BOTTOM stop -- to measure the seven spectrum
       stops against it, and it was restating both stops by hand. A restated
       literal is only correct until someone edits the rule, and nothing would
       have failed: the test would keep measuring a navbar that no longer
       existed and keep reporting contrast numbers for it. */
    --prism-navbar-glass: linear-gradient(180deg,
                              rgba(21, 24, 40, 0.65) 0%,
                              rgba(21, 24, 40, 0.35) 100%);

    /* Text ramp. */
    --prism-text:            #ffffff;
    --prism-text-2:          #c8d2e4;
    /* #99a5be, not the #8b96ad this was, and the reason is a measurement this
       file never took. Every contrast test here has been light-mode only —
       TestLightModeContrast, the card-ground fixture, all of it — so dark's
       equivalent number was simply never computed, and it was 3.98:1 on a card
       sitting where the blue and magenta mesh lobes overlap. Under AA, and it
       had been the whole time.

       The neutral veil moved it to 3.84 (a white veil raises the ground under
       light text, which lowers contrast; in light mode the same change raises
       it, which is why only dark is affected). 0.14 is not what makes this
       worth fixing — being asked the question for the first time is.

       Scaled from the original rather than rechosen, so the hue is unchanged
       and this stays the same grey, lighter. Clears 4.5:1 on the worst-case
       card ground, which is the mesh-overlap corner; anywhere else on the page
       it has more room. tests/test_dark_mode_contrast.py now asks. */
    --prism-text-muted:      #99a5be;
    --prism-text-dim:        rgba(255, 255, 255, 0.5);
    --prism-text-faint:      rgba(255, 255, 255, 0.4);

    /* Borders and depth. */
    --prism-border:          rgba(255, 255, 255, 0.1);
    --prism-border-strong:   rgba(255, 255, 255, 0.18);
    --prism-border-faint:    rgba(255, 255, 255, 0.06);
    --prism-shadow-soft:     rgba(0, 0, 0, 0.18);

    /* Accent washes. Alphas differ per mode because the same alpha does not
       carry the same weight over a near-black ground as over a light one. */

    /* Accent applied to borders and to link/label text. Borders strengthen on
       a light ground where washes weaken, so they are their own token rather
       than a reuse of --prism-tint-*. */
    /* NOTE: these two are declared on `body` further down, not here — they
       read the accent triplet, which the skins set on body[data-skin]. See
       ACCENT TEXT below. */
    /* Label colour for a filled semantic button. Dark mode fills with the
       luminous brand greens/oranges, where white text lands at 1.45:1 -- the
       label has to go dark. Light mode fills are deep, so white is correct. */
    --prism-on-semantic:     #11121c;

    /* Ink for a glyph or label sitting ON an accent fill. Looks like a synonym
       for --prism-on-semantic and is not: that one FLIPS to #ffffff in light,
       because the semantic colours flip under it (--prism-success is the
       luminous #00f2c3 in dark and the deep #005d4b in light). The nine accents
       are the SAME hex in both modes, so their ink must not flip either -- and
       it is pinned dark because every accent is light enough to carry dark ink,
       the closest being indigo at 4.5:1. White is what fails: measured on the
       rendered control it is 1.91:1 on teal, 2.15:1 on amber, 2.35:1 on green
       and 2.89:1 on orange, under the 3:1 WCAG 1.4.11 asks of the mark that
       communicates state. Do NOT add a .white-content override for this. */
    --prism-on-accent:       #11121c;

    /* Filled/active accent chips. Unlike the washes, these gain saturation on
       light — a 30% fill that reads as "selected" over near-black disappears
       over a light ground. */

    /* Semantic colours shift slightly between modes — the dark-mode greens in
       particular are too luminous to read on a light ground. */
    --prism-success:         #00f2c3;
    --prism-warning:         #ff8d72;
    --prism-danger:          #fd5d93;
    --prism-pending:         #f59e0b;
    --prism-success-wash:    rgba(0, 242, 195, 0.2);
    --prism-warning-wash:    rgba(255, 141, 114, 0.2);
    --prism-danger-wash:     rgba(253, 93, 147, 0.2);
    --prism-warning-border:  rgba(255, 141, 114, 0.35);
    /* The WASH's base colour, not the text colour's, exposed so a rule needing
       some other alpha can have one. The distinction is invisible here and
       load-bearing in light mode: see the light block. */
    --prism-success-rgb:     0, 242, 195;
    --prism-warning-rgb:     255, 141, 114;
    --prism-danger-rgb:      253, 93, 147;

    /* The landing page's 7-stop spectrum, shared so /home and the app's scroll
       progress bar cannot drift apart. home.html points --px-spectrum here.

       These stops are tuned for a near-black ground and do NOT survive the
       flip -- see the light override for the measurements and the retune. */
    /* The wordmark. Fixed hues — this is the company's name, not a surface, and
       it must not follow the accent skin: on the green skin the brand read
       green, on amber it read amber, which is the one string on screen that
       cannot be a user preference.

       It DOES follow light/dark, because a mode is not a skin. The dark stops
       are the brand's own blue/violet/magenta; the light ones are the same
       three hues at the depths this file already uses for accent text
       (--prism-accent-text is #0f5ea8, --prism-accent2-text is #912080),
       because #1d8cf8 as text measures 2.81:1 on a light card and the wordmark
       sits on the navbar, which is lighter still. Same identity, legible on
       both grounds. */
    --prism-wordmark: linear-gradient(135deg, #1d8cf8 0%, #8b5cf6 52%, #e14eca 100%);

    --prism-spectrum: linear-gradient(90deg, #ff5d73 0%, #ffab5e 16%, #ffe15d 32%,
                                      #5bd6a0 50%, #1d8cf8 68%, #8b5cf6 84%, #e14eca 100%);
    /* Unfilled rail behind the progress fill. Without it the bar has no ground
       of its own and "10% scrolled" is indistinguishable from "short page". */
    --prism-progress-track:  rgba(255, 255, 255, 0.07);
    /* Shadow under the bar. GEOMETRY flips here, not only colour, which is why
       this is the whole shadow and not a bare glow colour: a 14px
       omnidirectional bloom reads as luminous over near-black and as a smear
       over near-white, where a directional drop shadow separates instead. */
    --prism-progress-shadow: 0 0 14px rgba(139, 92, 246, .6);

    /* Generic document text, reclaimed from black-dashboard.css. Dark values
       reproduce exactly what the vendor rules produced (h1-h6 at 80% white,
       body copy at #525f7f) so decoupling changes light mode only. */
    --prism-heading:         rgba(255, 255, 255, 0.8);
    /* NOT the vendor's #525f7f. That is what `body { color }` has always been
       in black-dashboard.css, and reproducing it exactly is what the first
       pass did -- but the rendered audit measured it at 2.25:1 on a #27293d
       card. Preserving a value faithfully is not the same as it being right. */
    --prism-body:            #a8b2c8;
    --prism-input-bg:        rgba(222, 222, 222, 0.1);
    --prism-input-disabled:  rgba(255, 255, 255, 0.08);
}

/* ===== DERIVED ACCENT TOKENS — DECLARED ON body, NOT :root =====
   These reference --prism-accent-rgb, and the skins set that on
   body[data-skin=…]. A var() inside a custom property is substituted at
   the element the property is DECLARED on, not where it is used — so
   declaring these on :root captures :root's blue and every skin silently
   renders blue tints. Verified in a browser, not inferred: an alias on
   :root resolved to the :root value even with the token overridden on
   body; the same alias declared on body resolved correctly.
   body.white-content (0-1-1) still overrides these, as before. */
body {
    --prism-accent-border:   rgba(var(--prism-accent-rgb), 0.3);
    --prism-accent2-wash:    rgba(var(--prism-accent2-rgb), 0.15);
    --prism-fill-1:          rgba(var(--prism-accent-rgb), 0.3);
    --prism-fill-2:          rgba(var(--prism-accent-rgb), 0.5);
    --prism-tint-1:          rgba(var(--prism-accent-rgb), 0.05);
    --prism-tint-2:          rgba(var(--prism-accent-rgb), 0.08);
    --prism-tint-3:          rgba(var(--prism-accent-rgb), 0.12);
    --prism-tint-4:          rgba(var(--prism-accent-rgb), 0.18);

    /* NEUTRAL veil ladder — the tier this system never had, and the reason the
       app read as "a blue app" while /home read as "a dark app with a spectrum
       in it".

       The tints above are the accent at 5-18%, and they were backing every
       surface AT REST: the card fill, the card border, the card header and
       footer, and the whole left sidebar. So the accent was not an accent, it
       was a dye. Pick green and the furniture went green.

       The landing page does the opposite and it is worth being precise about
       what it does, because "translucent black" is the effect and not the
       mechanism: over a near-black ground it paints a translucent WHITE veil,
       which is what makes a card read as a pane of dark glass lifted off the
       page. Colour is spent only on things that mean something — buttons,
       icon tiles, badges, the active nav item, focus, hover, and the ambient
       mesh, which is where the skin still very much shows.

       Dark's two working values are its `.bento .cell` verbatim.

       A ladder rather than a hi/lo pair because the landing page is one too
       (.02 on the FAQ, .03 on chips, .04 on mock rows, .045->.012 on cards)
       and because the header and footer composite ON TOP of the card body:
       give them the same veil and they vanish into it. Named to parallel
       --prism-tint-1..4 so the migration reads as tint-N -> veil-N. */
    --prism-veil-1:          rgba(255, 255, 255, 0.012);
    --prism-veil-2:          rgba(255, 255, 255, 0.045);
    --prism-veil-3:          rgba(255, 255, 255, 0.08);

    /* Corner radii, for the two families where the value carries meaning: a
       card and a control. Deliberately NOT a scale for all 174 border-radius
       declarations in this file — the 6/8/10px small-element radii are fine as
       they are, and normalising them would be a large diff with a real
       regression surface and no visible payoff.

       18px is the landing page's --px-radius, and the jump from 12 is more than
       it sounds: a card is a large shape, and 12px on something 400px wide
       reads as a rectangle with the corners knocked off rather than as a soft
       panel. The pill is what the landing page uses for every button; at the
       app's current button heights it is only a few pixels from the 25px it
       replaces, which is why this is the cheap half of the change. */
    --prism-radius-card:     18px;
    --prism-radius-panel:    12px;
    --prism-radius-pill:     999px;

    /* The type scale. Six steps for TEXT — not for icons, which is the
       distinction that makes this safe to apply mechanically.

       It exists because the right drawer had no scale at all. Neither
       .right-sidebar nor .sidebar-content declares a font-size, so ~60
       drawer-scoped rules each picked their own literal off body's 14px in
       three different unit systems: `em` in .sidebar- and .tx-, `rem` in
       .cat-form-*, and raw `px` in .lew-* (10/11/13/18px, which scale with
       nothing at all). That produced ~32 distinct rendered sizes and, more
       visibly, two competing row systems: the asset detail rendered its rows
       at 11.2/12.32px while the category drilldown beside it rendered the
       same kind of information at 14px.

       rem, not em, and that is the load-bearing half. `em` compounds against
       whatever it lands in, so a value that reads as one size in the rule is
       another on screen: .sidebar-derived-tag's 0.72em nested inside
       .sidebar-kv-label's 0.8em put the AMORTIZED chip beside Loan Balance at
       8.06px. A scale whose steps change meaning by depth is not a scale.

       --prism-fs-base is body's own 0.875rem, so a step is a deliberate
       departure from the page's text size rather than a fresh guess.

       An icon sized off its own label — .sidebar-section-header i,
       .sidebar-title .cat-icon-ph, .tx-similar-pending-chip i — stays `em` and
       must NOT take a step here. That relationship is the point of those
       rules, and converting them to an absolute is how this breaks. */
    --prism-fs-2xs:          0.6875rem;  /* 11px — micro chips, derived tags */
    --prism-fs-xs:           0.75rem;    /* 12px — badges, hints, field labels */
    --prism-fs-sm:           0.8125rem;  /* 13px — row labels, secondary values */
    --prism-fs-base:         0.875rem;   /* 14px — body: row values, row names */
    --prism-fs-lg:           1rem;       /* 16px — section headers, headlines */
    --prism-fs-xl:           1.25rem;    /* 20px — big numerals, drawer title */

    /* The brand gradient. On the default `prism` skin this resolves to exactly
       the landing page's --px-brand:
           linear-gradient(135deg, #1d8cf8 0%, #8b5cf6 52%, #e14eca 100%)
       and home.html now points --px-brand here, the same way --px-spectrum
       already points at --prism-spectrum, so the two cannot drift.

       Stop ORDER is accent -> accent3 -> accent2, not the numeric order. The
       landing page runs blue through violet to magenta, and accent3 is the
       violet; accent -> accent2 -> accent3 would put the magenta in the middle
       and read as a different mark.

       Holds no colour of its own — every stop is a skin token — so it follows
       all ten skins and needs no light value. That is what STRUCTURAL in
       tests/test_light_mode_css.py exempts, and
       test_structural_tokens_really_are_colourless is what stops a literal
       being written in here later; verified by writing one and watching it
       fail. */
    --prism-brand: linear-gradient(135deg, var(--prism-accent) 0%,
                                   var(--prism-accent3) 52%,
                                   var(--prism-accent2) 100%);

    /* ===== ACCENT TEXT =====
       The accent, adjusted until it is legible AS TEXT, which the raw accent is
       not: measured on a card, the ten skins run 2.71:1 (indigo) to 5.4:1, so
       seven of them fail AA used directly.

       These were literals — #1d8cf8 and #e14eca in dark, #0f5ea8 and #912080 in
       light — and that is the bug behind the sidebar icons staying blue on the
       red skin. Sixty-eight rules read these two tokens, so a token named for
       the accent that never followed it was quietly pinning most of the app's
       small text and every nav icon to the default palette. The drawer's close
       button already had a comment about it and worked around it with an opaque
       backing rather than fixing the token.

       color-mix rather than ten hand-tuned pairs: the adjustment is the same
       operation for every skin, so expressing it once means an eleventh skin
       needs no new contrast values. Toward white in dark and black in light,
       because "legible" points in opposite directions on the two grounds.

       60% and 45% are the measured floors plus margin. At 60% toward white the
       worst skin (red) is 4.84:1 on the worst-case card ground; at 45% toward
       black the worst (teal) is 4.7:1. Every skin clears AA in both modes —
       which the fixed values did not: #1d8cf8 was 3.34:1 in dark. */
    --prism-accent-text:  color-mix(in srgb, var(--prism-accent) 60%, #ffffff);
    --prism-accent2-text: color-mix(in srgb, var(--prism-accent2) 60%, #ffffff);
}



body.white-content {
    --prism-color-scheme: light;

    /* Translucent, mirroring dark's rgba(255,255,255,.03/.06). The first pass
       wrote these as opaque hexes, which is why .report-table-container,
       .prism-card-subtle and .plan-card rendered as solid panels sitting next
       to glass prism cards. Light has to mirror dark's OPACITY, not only its
       hues. */
    --prism-surface-2:       rgba(22, 27, 41, 0.03);
    --prism-surface-hover:   rgba(22, 27, 41, 0.06);
    --prism-surface-raised:  #f0f2f7;
    /* Transparent, exactly like dark. The ambient grid is only 3% alpha, so any
       veil erases it: the .70 white this replaced transmitted 30% of it, which
       rendered the grid at 0.9% (invisible) and computed the card to #e2e9f8 —
       white in all but name. Cards were transparent here before the token
       layer existed too; that is the look, and it is worth the constraint.

       The constraint: card text now sits on the mesh-tinted ground, roughly ten
       points darker than a panel would be, which costs one tier of the text
       ramp. --prism-text-2 and --prism-text-muted below are darkened to pay for
       it. (Pre-token light mode ran muted at ~3.9:1 here and was never AA; this
       keeps the look and fixes that rather than inheriting it.)

       Separation is carried by .prism-card's border and box-shadow, not by a
       fill — same as dark. */
    --prism-card-bg:         transparent;
    --prism-panel:           linear-gradient(180deg, #f0f2f7 0%, #e8ebf2 100%);
    --prism-overlay:         rgba(240, 242, 247, 0.98);

    /* Light's twin. The ground sits between the page base (#dde1ea) and
       --prism-surface-raised (#f0f2f7): above the page so the drawer lifts,
       below the floating-layer tier so a dropdown opened inside it still
       reads as above the drawer.

       The bloom runs at less than half of dark's. .34 over a light ground
       stops reading as a light source and starts reading as a lavender wash
       over the corner — the accent SUBTRACTS light here where it adds it on
       near-black. .16 keeps it a soft corner light. */
    --prism-drawer-ground:   #eef0f6;
    --prism-drawer-bloom:    0.16;
    --prism-drawer-dot:      rgba(22, 27, 41, 0.085);
    --prism-navbar-glass: linear-gradient(180deg,
                              rgba(255, 255, 255, 0.75) 0%,
                              rgba(255, 255, 255, 0.55) 100%);

    --prism-text:            #161b29;
    /* Darker than the surface-only maths would suggest, because prism cards are
       transparent: these land on the mesh-tinted ground, not on a panel. At
       #4a5468/#59616f they measured 4.49 and 3.68 there. */
    --prism-text-2:          #3f4757;
    --prism-text-muted:      #495060;
    /* Composited over --prism-surface these give 6.2:1 and 4.5:1. The first
       draft used .6/.45, which put the faint tier at 2.8:1 -- the uppercase
       eyebrow labels on every card were visibly washed out. A dimmer ground
       leaves less room than a near-white one did. */
    --prism-text-dim:        rgba(22, 27, 41, 0.62);
    --prism-text-faint:      rgba(22, 27, 41, 0.52);

    --prism-border:          rgba(22, 27, 41, 0.14);
    --prism-border-strong:   rgba(22, 27, 41, 0.24);
    --prism-border-faint:    rgba(22, 27, 41, 0.08);
    --prism-shadow-soft:     rgba(22, 27, 41, 0.06);

    --prism-tint-1:          rgba(var(--prism-accent-rgb), 0.04);
    --prism-tint-2:          rgba(var(--prism-accent-rgb), 0.06);
    --prism-tint-3:          rgba(var(--prism-accent-rgb), 0.09);
    --prism-tint-4:          rgba(var(--prism-accent-rgb), 0.14);

    /* The veil, inverted. WHITE here too, which is the one place this file's
       otherwise absolute rule — every light token is dark's rgba(22,27,41,a) —
       is deliberately broken. A card lifts off a light ground only by being
       LIGHTER than it; a dark veil over a light page reads as a smudge, not as
       a raised surface. Computed, it also lands the card ground at
       rgb(193,193,224), which fails the measured envelope in
       test_card_ground_matches_what_the_browser_actually_paints by two points
       of green. So the mirror is of the STRUCTURE, not of the arithmetic.

       Heavier than dark's numbers, by a lot, and that is not a fudge: the same
       alpha does not carry the same weight in both directions. .045 white over
       #0a0b12 moves the ground about 11 points and reads as a distinct pane;
       .045 white over a ~rgb(201,201,232) light ground moves it 2 and reads as
       nothing at all. These lift by roughly the same visible amount dark does.

       Three ceilings, all measured, and the first one is why this is .18 and
       not the .22 the arithmetic alone would allow:

       1. tests/audit_contrast.py bails out of a node whose gradient has ANY
          stop above 0.25 (`p.a > 0.25` -> washOnly false -> imaged -> the node
          is skipped). That is silent, and the comment there records it
          dropping every card from the sweep twice, 277 measured nodes to 197,
          with each run "looking like a contrast improvement". .24 at the top
          of this ladder is the hard cap and .18 leaves the card real margin.
       2. The ambient grid is 3% alpha and needs 2% to survive, so the fill
          cannot exceed .333. .18 transmits 2.46%.
       3. The card ground has to stay inside the envelope sampled from rendered
          screenshots. .18 computes to rgb(210,211,236); the bound is
          r<=220, g<=226, b<=245.

       Numbers 2 and 3 are arithmetic and hold. Number 1 is a code path. None
       of the three is a substitute for looking at it — see the commit message
       for what still needs measuring against real pixels. */
    --prism-veil-1:          rgba(255, 255, 255, 0.05);
    --prism-veil-2:          rgba(255, 255, 255, 0.18);
    --prism-veil-3:          rgba(255, 255, 255, 0.24);

    --prism-accent-border:   rgba(var(--prism-accent-rgb), 0.25);
    /* NOT var(--prism-accent): #1d8cf8 is 2.81:1 on a card and 2.44:1 on an
       accent-washed chip, which is where this colour actually gets used. */
    /* Toward BLACK here, where dark mixes toward white — "legible" points in
       opposite directions on the two grounds, which is why this is a genuine
       mode asymmetry and not a restatement. See ACCENT TEXT on `body`. */
    --prism-accent-text:  color-mix(in srgb, var(--prism-accent) 45%, #000000);
    --prism-accent2-text: color-mix(in srgb, var(--prism-accent2) 45%, #000000);
    --prism-on-semantic:     #ffffff;
    --prism-accent2-wash:    rgba(var(--prism-accent2-rgb), 0.1);

    --prism-fill-1:          rgba(var(--prism-accent-rgb), 0.4);
    --prism-fill-2:          rgba(var(--prism-accent-rgb), 0.6);

    /* Darker than the obvious light-mode variants because these are read as
       TEXT: the dark-mode green #00f2c3 measures 1.19:1 on a light card, which
       made income totals effectively invisible.

       Retuned a second time once cards went glass. The first values were
       measured against the flat --prism-surface token (#e6e9f0) and cleared
       4.5:1 there — but that token stopped backing anything when the card lost
       its fill, and the real ground is the mesh-tinted page, around
       rgb(190, 197, 233). On THAT they measured 3.4-3.6:1. Verified against
       pixels sampled from rendered cards, which span rgb(179,203,230) to
       rgb(211,216,234); these clear 4.5:1 across the whole range. */
    --prism-success:         #005d4b;
    --prism-warning:         #8a3823;
    --prism-danger:          #97244c;
    /* #d97706 (the old chart value) is 2.62:1 on a card. */
    --prism-pending:         #7b4409;
    --prism-success-wash:    rgba(0, 137, 95, 0.14);
    --prism-warning-wash:    rgba(192, 80, 48, 0.14);
    --prism-danger-wash:     rgba(211, 52, 108, 0.14);
    --prism-warning-border:  rgba(192, 80, 48, 0.35);
    /* Here is why these exist. In DARK the wash's base and the semantic hex are
       the same colour, so `color-mix(in srgb, var(--prism-danger) 30%,
       transparent)` looks like a correct way to get an arbitrary alpha. In
       LIGHT they are not: the hexes above are darkened until they clear AA as
       TEXT, and that darkening is wrong for a wash — mixed down to 10-30% over
       a light card, #97244c reads as grey dinge rather than red. The wash bases
       stayed bright precisely because they are never text.

       So a rule wanting danger at some alpha the -wash token does not offer
       must read rgba(var(--prism-danger-rgb), A), never color-mix on the hex.
       The bug is invisible in dark, which is where it would be written. */
    --prism-success-rgb:     0, 137, 95;
    --prism-warning-rgb:     192, 80, 48;
    --prism-danger-rgb:      211, 52, 108;
    /* The spectrum is NOT mode-invariant, however brand-fixed it looks.

       The bar rides the navbar's bottom edge, so its ground is the BOTTOM of
       the navbar's 180deg glass gradient (0.55 white, not the 0.75 at the top)
       over the mesh, and then the bar's own track: rgb(213, 215, 225)
       composited. Measured there -- the layer actually behind the fill -- the
       seven dark stops run 2.07 / 1.30 / 1.11 / 1.26 / 2.38 / 2.95 / 2.38 :1.
       Not one of them clears 3:1, and the orange-to-mint middle HALF is
       effectively white on white, which is exactly the span a part-scrolled
       page shows: it reads as a broken bar rather than a dim one. No amount of
       extra height fixes a 1.11:1 stop.

       Same seven hues in the same order, darkened until each clears 3.5:1 on
       that ground. Only orange/yellow/mint move much; the ends barely shift,
       so it still reads as the brand gradient. Measure through EVERY layer:
       skipping the track reports these at 4.3:1, and measuring the navbar's
       top edge rather than its bottom adds another 0.25 on top of that. Both
       shortcuts shipped once. 3.5 and not 4.5 because the bar is decorative
       (aria-hidden) and WCAG 1.4.11's non-text floor is 3:1; the margin
       absorbs the nine skins, whose mesh tints move this ground either way.

       The olive yellow is the honest cost of a yellow on a light ground, not a
       hue error -- #ffe15d cannot be both itself and visible here.

       The landing page is unaffected: it never carries .white-content. */
    --prism-wordmark: linear-gradient(135deg, #0f5ea8 0%, #5b21b6 52%, #912080 100%);

    --prism-spectrum: linear-gradient(90deg, #e0001e 0%, #b25500 16%, #846b00 32%,
                                      #1c7e53 50%, #016dd6 68%, #7d46f9 84%, #c51daa 100%);
    --prism-progress-track:  rgba(22, 27, 41, 0.10);
    --prism-progress-shadow: 0 1px 4px rgba(22, 27, 41, 0.28);
    --prism-heading:         #161b29;
    --prism-body:            #2c3446;
    --prism-input-bg:        #eceff5;
    --prism-input-disabled:  #d5dae4;
}

/* ===== BODY TYPE =====
   The other half. Four vendor rules set the family and all four need answering,
   because dropping the Poppins <link> without them does not fall through to
   these tokens — it falls through to the browser's default sans:
     black-dashboard.css:84     body
     black-dashboard.css:6715   .tooltip
     black-dashboard.css:6840   .popover
     black-dashboard.css:16485  button, input, optgroup, select, textarea
   The last one is the one that bites: form controls do NOT inherit
   font-family from their ancestors, so a body rule alone leaves every input,
   select and button on the old face.

   `size-adjust` is why these are self-hosted. Plus Jakarta Sans has a larger
   x-height than Poppins at the same font-size, so an unadjusted swap grows
   every line box a little — which is invisible on a heading and is a wrapped
   `Parent -> Child` category pair on a transaction row. 97% pins the rendered
   x-height back to what Poppins was producing, so the dense tables in
   asset-transactions.html and transactions.html keep their current line heights and
   nothing has to be re-tuned page by page.

   It belongs on the @font-face and not here — a metric override is a property
   of the FACE, and applying it as a transform at each call site would miss
   every rule that inherits.

   Not permanent. It is scaffolding that makes this commit revertable in one
   line; removing it later is a deliberate choice to accept the face's own
   metrics, made after looking at the tables, not before. */
body {
    font-family: var(--prism-font-body);
}

/* Form controls do not inherit font-family. Matching the vendor's own selector
   list rather than `.form-control`, because it also covers the bare <select>
   and <textarea> elements the app uses outside that class. */
body button,
body input,
body optgroup,
body select,
body textarea {
    font-family: var(--prism-font-body);
}

body .tooltip,
body .popover {
    font-family: var(--prism-font-body);
}

/* ===== DISPLAY TYPE =====
   Headings, card titles, section titles and the big numerals. Everything here
   is a large, sparse shape where the display face's character shows and its
   metrics cannot push a dense table around — which is why this half of the
   typeface change is separate from, and much safer than, the body face.

   `body`-prefixed to clear black-dashboard's element-level h1-h6 rules, which
   set the family alongside the colour. The vendor's `.card h1..h6` (0-1-1) is
   matched by an equally specific rule further down this file, so these run at
   0-0-2 on the bare elements and let that one keep its job.

   Tighter tracking comes with the face: Space Grotesk at display sizes is loose
   at 0 letter-spacing, and the landing page runs -1.4px on the hero and -0.5px
   on section heads. `text-wrap: balance` is what stops a two-line heading
   leaving one orphaned word, which is far more visible at 18px radius and
   generous padding than it was before. */
body h1, body h2, body h3, body h4, body h5, body h6 {
    font-family: var(--prism-font-display);
    letter-spacing: -0.02em;
    text-wrap: balance;
}

body .prism-card-title,
body .prism-section-title {
    font-family: var(--prism-font-display);
}

/* .prism-title is the big page heading — the dashboard's WELCOME line, and the
   same treatment on Reports, Transactions and the rest. It stays on Poppins,
   which is what it has always rendered in; sweeping it into the display face
   with the rest of the headings changed the most prominent type on every page
   as a side effect of a typeface swap it was never really part of. */
body .prism-title {
    font-family: var(--prism-font-title);
}

/* Numerals. A display face with real character is worth most on the figures
   people actually read off these pages — balances, totals, DSCR, cap rate —
   and Space Grotesk's are unusually even in width, which keeps a column of
   money from looking ragged. */
body .stats-metric-value,
body .stats-hero-spotlight-value,
body .sidebar-stat-value,
body .tx-list-row-amount,
body .perf-chip-value {
    font-family: var(--prism-font-display);
    letter-spacing: -0.01em;
}

/* ===== GRADIENT TEXT EFFECTS ===== */
/* --prism-brand, not a local two-stop: this is the same device as the landing
   page's `.grad` and should be the same gradient. It ran accent2 -> accent
   (magenta to blue), which is the brand mark backwards. */
.prism-gradient-text {
    background: var(--prism-brand);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    font-weight: 700;
}

.prism-title {
    font-weight: 800;
    text-shadow: 0 4px 20px rgba(var(--prism-accent2-rgb), 0.3);
}

.white-content .prism-title {
    text-shadow: 0 2px 10px rgba(var(--prism-accent2-rgb), 0.15);
}

/* ===== SECTION TITLES ===== */
.prism-section-title {
    font-size: 2.5rem;
    font-weight: 800;
    margin-bottom: 20px;
    text-shadow: 0 4px 20px rgba(var(--prism-accent2-rgb), 0.2);
    line-height: 1.2;
}

.white-content .prism-section-title {
    text-shadow: 0 2px 10px rgba(var(--prism-accent2-rgb), 0.15);
}

.prism-section-subtitle {
    /* --prism-text-2, not --prism-text-muted: this sits on the open page
       ground, where the ambient mesh composites underneath it. Muted measures
       4.59:1 against the flat base but only 4.20:1 over the tinted ground --
       and at 1.1rem/300 this is 17.6px regular, which is not WCAG large text,
       so it needs the full 4.5:1. Muted stays correct everywhere it is used
       inside a card (5.14:1 there); it is the open ground it cannot hold. */
    font-size: 1.1rem;color: var(--prism-text-2);
    margin-bottom: 30px;
    line-height: 1.6;
    font-weight: 300;
}

/* ===== CENTERED PAGE HEADER ===== */
/* Used in: add_asset.html, edit_asset.html — both carried a byte-identical copy
   of this, alongside a redeclaration of .prism-gradient-text that silently won
   over the rule above, so a change to the shared one would have landed on every
   page except those two.

   The inline-block is scoped to this header rather than folded into
   .prism-gradient-text itself: that class is on 25+ templates, on block headings
   and on spans alike, and shrink-wrapping all of them would change their box
   model and stop their margins collapsing. Here it is load-bearing --
   background-clip: text maps --prism-brand across the ELEMENT box, so a
   full-width centered block renders only the middle of the gradient (violet)
   instead of the blue -> violet -> magenta sweep across the glyphs. */
.page-header-centered {
    text-align: center;
    margin-bottom: 40px;
}

.page-header-centered .prism-gradient-text {
    display: inline-block;
}

/* ===== CARD STYLES =====
   The card is a pane of glass over the ambient ground, and its fill is NEUTRAL.
   It used to be the accent at 8% over the accent at 5%, inside an accent
   border, which is what made every surface in the app take the skin's hue. The
   angle is 160deg rather than 180 for the same reason the landing page's is:
   a diagonal reads as a lit sheet, a vertical one as a swatch.

   Hover keeps its accent. That split is the whole policy — at rest a surface
   is neutral, on interaction it is the skin — and de-tinting the resting state
   makes the hover MORE legible, not less: a tint-4 selection used to sit on a
   tint-2 card, a delta of .10 in the same hue, and now it is the full .18 and
   a hue change as well. */
.prism-card {
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-card);
    padding: 25px;
    transition: transform 0.3s cubic-bezier(.2, .7, .3, 1),
                border-color 0.3s ease, box-shadow 0.3s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 2px 10px var(--prism-shadow-soft);
}

.prism-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(circle at top right, rgba(var(--prism-accent2-rgb), 0.05) 0%, transparent 70%);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.prism-card:hover::before {
    opacity: 1;
}

/* The landing page's lift: further, on a soft dark shadow rather than an accent
   glow, with the accent kept on the border where it still reads as "this one".
   A coloured bloom under a card was legible when the card itself was tinted; on
   neutral glass it looks like the card is leaking. Easing is the landing page's
   single shared curve — a slight overshoot, so the card arrives rather than
   ramps. */
.prism-card:hover {
    border-color: var(--prism-accent-border);
    transform: translateY(-4px);
    box-shadow: 0 20px 50px -24px rgba(0, 0, 0, 0.8);
}

/* ===== CARD SMALL ===== */
.prism-card-sm {
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-card);
    transition: transform 0.3s cubic-bezier(.2, .7, .3, 1),
                border-color 0.3s ease, box-shadow 0.3s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 2px 10px var(--prism-shadow-soft);
}

.prism-card-sm::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(circle at top right, rgba(var(--prism-accent2-rgb), 0.05) 0%, transparent 70%);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.prism-card-sm:hover::before {
    opacity: 1;
}

.prism-card-sm:hover {
    border-color: var(--prism-accent-border);
    transform: translateY(-4px);
    box-shadow: 0 20px 50px -24px rgba(0, 0, 0, 0.8);
}

/* ===== CARD WITH STRONGER GLOW =====
   The one card that KEEPS its accent border. This is the highlighted variant —
   the landing page's `.tier.featured`, which is likewise the only surface there
   with a brand-coloured edge. De-tinting it too would leave nothing to
   distinguish it from a plain card, which is the opposite of its job. Its fill
   still goes neutral, one tier heavier than a plain card. */
.prism-card-glow {
    background: linear-gradient(160deg, var(--prism-veil-3) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    border: 1px solid var(--prism-accent-border);
    border-radius: var(--prism-radius-card);
    padding: 25px;
    transition: transform 0.3s cubic-bezier(.2, .7, .3, 1),
                border-color 0.3s ease, box-shadow 0.3s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 2px 15px var(--prism-shadow-soft);
}

/* The highlighted card keeps a trace of accent in its shadow — it is the one
   surface allowed to glow, the same way .tier.featured is on /home. */
.prism-card-glow:hover {
    border-color: rgba(var(--prism-accent-rgb), 0.5);
    transform: translateY(-4px);
    box-shadow: 0 24px 56px -26px rgba(var(--prism-accent-rgb), 0.5);
}

/* ===== SUBTLE CARD (for working interfaces) ===== */
.prism-card-subtle {
    background: var(--prism-surface-2);
    border: 1px solid var(--prism-border);
    border-radius: 10px;
    padding: 20px;
    transition: all 0.3s ease;
}

.prism-card-subtle:hover {
    background: var(--prism-surface-hover);
    border-color: rgba(var(--prism-accent-rgb), 0.3);
}

/* ===== CARD HEADER =====
   Flat --prism-veil-3, not the card's own gradient, and that is the one place
   this change needed real thought rather than a substitution.

   The header composites ON TOP of the card body. It used to paint accent
   .10 -> .05 over a card painting accent .08 -> .05, so it separated by a
   sliver of extra accent. Hand both the same neutral pair and the header
   disappears into the body entirely — same colour, same angle, no edge. One
   tier heavier, flat so it does not fight the body's diagonal, and the accent
   hairline in ::after below stays accent: it is now the thing actually drawing
   the line, so it is load-bearing rather than decorative. Same for the footer. */
.prism-card-header {
    padding: 20px 25px;
    margin: -25px -25px 20px -25px;border-bottom: 1px solid var(--prism-border-faint);
    background: var(--prism-veil-3);
    border-radius: var(--prism-radius-card) var(--prism-radius-card) 0 0;
    position: relative;
}

.prism-card-header::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.3) 50%, transparent 100%);
}

.white-content .prism-card-header::after {
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.2) 50%, transparent 100%);
}

/* ===== CARD TITLE ===== */
.prism-card-title {color: var(--prism-text);
    font-weight: 600;
    margin: 0;
    line-height: 1.4;
}

/* ===== CARD BODY ===== */
.prism-card-body {
    padding: 20px 25px;
    position: relative;
    color: var(--prism-text);
}


/* ===== CARD FOOTER ===== */
.prism-card-footer {
    padding: 20px 25px;
    margin: 20px -25px -25px -25px;border-top: 1px solid var(--prism-border-faint);
    background: var(--prism-veil-3);
    border-radius: 0 0 var(--prism-radius-card) var(--prism-radius-card);
    position: relative;
}

.prism-card-footer::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.3) 50%, transparent 100%);
}

.white-content .prism-card-footer::before {
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.2) 50%, transparent 100%);
}

/* ===== AUTH CARD ACTIONS =====
   The row of secondary links under an auth form (register / forgot password /
   back to login). It exists because login.html laid this out as a bootstrap
   `.row` of two `col col-md-6` halves, one of them `text-right` -- which put
   the controls somewhere different at every width and in every configuration:

     - With signup closed (the beta default) only the right-aligned half
       renders, so "Forgot password?" sat right-aligned inside the LEFT half of
       the card, i.e. floating just left of centre under a centred form.
     - `col` is equal-width at every breakpoint, so on a phone the two halves
       stayed side by side at ~140px each and the pills' text wrapped out of
       their own backgrounds and past the footer's edge.

   Centred flex-wrap is the one arrangement that reads the same whether there
   are one or two controls: one control centres, two centre as a pair, and a
   narrow screen stacks them still centred rather than pinning one to each
   edge. It also blockifies the children, which is what stops an inline <a>
   pill's vertical padding spilling out of the footer. */
.prism-auth-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 12px 20px;
}

/* The pills keep their own text on one line and wrap as whole units instead,
   since a pill broken across two lines loses its shape. */
.prism-auth-actions > * {
    white-space: nowrap;
}

/* The tertiary auth action -- "Forgot password?" was a `prism-btn-danger`
   pill, which read as a warning and competed with the Register CTA beside it
   for the eye. Quiet text keeps the hierarchy Login > Register > this, and the
   vertical padding holds the tap target to the pills' height so a wrapped row
   still lines up. */
.prism-auth-link {
    color: var(--prism-text-2);
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.5px;
    text-decoration: none;
    padding: 8px 2px;
    border-bottom: 1px solid transparent;
    transition: color 0.3s ease, border-color 0.3s ease;
}

.prism-auth-link:hover,
.prism-auth-link:focus {
    color: var(--prism-accent-text);
    border-bottom-color: rgba(var(--prism-accent-rgb), 0.45);
    text-decoration: none;
}

/* ===== BUTTON STYLES ===== */
/* The primary action carries the brand mark, as it does on /home. Was a local
   accent2 -> accent3 two-stop, which on the default skin is the magenta and
   violet ends of --prism-brand with the blue missing. */
.prism-btn-primary {
    background: var(--prism-brand);
    color: #ffffff;
    border: none;
    padding: 12px 30px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.3);
    cursor: pointer;
}

.prism-btn-primary::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.prism-btn-primary:hover::before {
    width: 300px;
    height: 300px;
}

.prism-btn-primary:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 12px 35px rgba(var(--prism-accent2-rgb), 0.5);
    color: #ffffff;
}

.prism-btn-primary:active {
    transform: translateY(0) scale(0.98);
}

/* ===== BUTTON INFO ===== */
.prism-btn-info {
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%);
    color: #ffffff;
    border: none;
    padding: 12px 30px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(var(--prism-accent-rgb), 0.3);
    cursor: pointer;
}

.prism-btn-info::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.prism-btn-info:hover::before {
    width: 300px;
    height: 300px;
}

.prism-btn-info:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 12px 35px rgba(var(--prism-accent-rgb), 0.5);
    color: #ffffff;
}

.prism-btn-info:active {
    transform: translateY(0) scale(0.98);
}

/* ===== OWNERSHIP TOGGLE (TOTAL / MY %) ===== */
/* No wrapper pill — buttons stand as independent prism-btn-sm pills */
.prism-ownership-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    box-shadow: none;
    padding: 0;
}

.prism-ownership-toggle .btn {
    margin: 0;
    padding: 8px 20px;
    border-radius: var(--prism-radius-pill);
    border: 2px solid var(--prism-accent2);
    background: transparent !important;
    background-image: none !important;
    color: var(--prism-accent2-text);
    box-shadow: none;
    letter-spacing: 1px;
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1;
    text-transform: uppercase;
    transition: all 0.4s ease;
    cursor: pointer;
    outline: none;
    position: relative;
}

.prism-ownership-toggle .btn:hover {background: var(--prism-accent2-wash) !important;
    background-image: none !important;
    transform: translateY(-2px) scale(1.02);
    color: #ffffff;
    box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.25);
    border-color: var(--prism-accent3);
}

.prism-ownership-toggle .btn.active {
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%) !important;
    background-image: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%) !important;
    color: #ffffff;
    border-color: transparent;
    box-shadow: 0 8px 25px rgba(var(--prism-accent-rgb), 0.3);
}

.prism-ownership-toggle .btn.active:hover {
    transform: translateY(-2px) scale(1.02);
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%) !important;
    background-image: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%) !important;
    color: #ffffff;
    box-shadow: 0 12px 35px rgba(var(--prism-accent-rgb), 0.5);
}

.prism-ownership-toggle .btn input[type="radio"] {
    display: none;
}

.white-content .prism-ownership-toggle .btn.active {
    box-shadow: 0 8px 18px rgba(var(--prism-accent-rgb), 0.35);
}

/* ===== BUTTON DANGER ===== */
.prism-btn-danger {
    background: linear-gradient(135deg, #fd5d93 0%, #ff7aa8 100%);
    color: #ffffff;
    border: none;
    padding: 12px 30px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(253, 93, 147, 0.3);
    cursor: pointer;
}

.prism-btn-danger::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.prism-btn-danger:hover::before {
    width: 300px;
    height: 300px;
}

.prism-btn-danger:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 12px 35px rgba(253, 93, 147, 0.5);
    color: #ffffff;
}

.prism-btn-danger:active {
    transform: translateY(0) scale(0.98);
}

/* ===== BUTTON WARNING ===== */
.prism-btn-warning {
    background: linear-gradient(135deg, #ff8d72 0%, #ffa366 100%);
    color: #ffffff;
    border: none;
    padding: 12px 30px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(255, 141, 114, 0.3);
    cursor: pointer;
}

.prism-btn-warning::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.prism-btn-warning:hover::before {
    width: 300px;
    height: 300px;
}

.prism-btn-warning:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 12px 35px rgba(255, 141, 114, 0.5);
    color: #ffffff;
}

.prism-btn-warning:active {
    transform: translateY(0) scale(0.98);
}

/* ===== BUTTON SUCCESS ===== */
.prism-btn-success {
    background: linear-gradient(135deg, #00f2c3 0%, #00bf9a 100%);
    color: #ffffff;
    border: none;
    padding: 12px 30px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(0, 242, 195, 0.3);
    cursor: pointer;
}

.prism-btn-success::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: translate(-50%, -50%);
    transition: width 0.6s, height 0.6s;
}

.prism-btn-success:hover::before {
    width: 300px;
    height: 300px;
}

.prism-btn-success:hover {
    transform: translateY(-2px) scale(1.02);
    box-shadow: 0 12px 35px rgba(0, 242, 195, 0.5);
    color: #ffffff;
}

.prism-btn-success:active {
    transform: translateY(0) scale(0.98);
}

/* ===== BUTTON SECONDARY ===== */
.prism-btn-secondary {
    background: transparent;
    border: 2px solid var(--prism-accent2);
    color: var(--prism-accent2-text);
    padding: 10px 28px;
    border-radius: var(--prism-radius-pill);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    transition: all 0.4s ease;
    position: relative;
    cursor: pointer;
}

.prism-btn-secondary:hover {background: var(--prism-accent2-wash);
    transform: translateY(-2px) scale(1.02);
    color: var(--prism-accent2-text);
    box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.25);
    border-color: var(--prism-accent3);
}

.prism-btn-secondary:active {
    transform: translateY(0) scale(0.98);
}

.prism-btn-secondary.active {
    background: linear-gradient(135deg, #804686 0%, #b309ae 100%);
    border-color: #804686;
    color: #ffffff;
    box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.25);
}

.prism-btn-secondary.active:hover {
    background: linear-gradient(135deg, #804686 0%, #b309ae 100%);
    border-color: #804686;
    color: #ffffff;
    box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.25);
}

/* ===== BUTTON SMALL ===== */
.prism-btn-sm {
    padding: 8px 20px;
    font-size: 0.85rem;
    letter-spacing: 0.5px;
}

/* Split allocation remove icon button */
.split-remove-btn {
    padding: 6px;
    width: 32px;
    height: 32px;
    min-width: 32px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.95rem;
    letter-spacing: 0;
    line-height: 1;
    box-shadow: 0 6px 18px rgba(253, 93, 147, 0.28);
}

/* Split remaining banner */
.split-remaining {
    margin: 10px 0 20px;
    padding: 12px 14px;
    border-radius: 8px;
    border-left: 3px solid #ef4444;
    background: rgba(239, 68, 68, 0.08);
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 700;
    color: #ef4444;
    letter-spacing: 0.2px;
}

.split-remaining .split-remaining-label {
    text-transform: uppercase;
    font-size: var(--prism-fs-xs);
    opacity: 0.9;
}

.split-remaining .split-remaining-amount {
    font-size: var(--prism-fs-lg);
}

.split-remaining.valid {
    background: rgba(34, 197, 94, 0.1);
    border-left-color: #22c55e;
    color: #22c55e;
}

.split-remaining.invalid {
    background: rgba(239, 68, 68, 0.12);
    border-left-color: #ef4444;
    color: #ef4444;
}

/* ===== ACTION BUTTONS (for inline actions in lists/tables) ===== */
.prism-action-btn {
    padding: 6px 10px;
    border: none;
    background: transparent;
    cursor: pointer;
    border-radius: 6px;
    font-size: 0.85em;
    transition: all 0.3s ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.prism-action-btn i {
    font-size: 1em;
}

.prism-action-btn-info {
    color: var(--prism-accent-text);
}

.prism-action-btn-info:hover {background: var(--prism-tint-4);
    transform: scale(1.1);
    box-shadow: 0 2px 8px rgba(var(--prism-accent-rgb), 0.3);
}

.prism-action-btn-danger {
    color: var(--prism-danger);
}

.prism-action-btn-danger:hover {background: var(--prism-danger-wash);
    transform: scale(1.1);
    box-shadow: 0 2px 8px rgba(253, 93, 147, 0.3);
}

.prism-action-btn-success {color: var(--prism-success);
}

.prism-action-btn-success:hover {background: var(--prism-success-wash);
    transform: scale(1.1);
    box-shadow: 0 2px 8px rgba(0, 242, 195, 0.3);
}

.prism-action-btn-warning {
    color: var(--prism-warning);
}

.prism-action-btn-warning:hover {background: var(--prism-warning-wash);
    transform: scale(1.1);
    box-shadow: 0 2px 8px rgba(255, 141, 114, 0.3);
}

/* White content mode */

/* ===== BUTTON GLOW ANIMATION ===== */
.prism-btn-glow {
    animation: prism-pulse-glow 3s ease-in-out infinite;
}

@keyframes prism-pulse-glow {
    0%, 100% {
        box-shadow: 0 8px 25px rgba(var(--prism-accent2-rgb), 0.3);
    }
    50% {
        box-shadow: 0 10px 35px rgba(var(--prism-accent2-rgb), 0.5);
    }
}

/* ===== BADGES ===== */
.prism-badge-primary {
    background: linear-gradient(135deg, var(--prism-accent2) 0%, var(--prism-accent3) 100%);
    color: #ffffff;
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

.prism-badge-info {background: var(--prism-tint-4);
    color: var(--prism-accent-text);
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

.prism-badge-success {background: var(--prism-success-wash);color: var(--prism-success);
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

.prism-badge-warning {background: var(--prism-warning-wash);
    color: var(--prism-warning);
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

/* Pending status — distinct amber tint so it sits cleanly next to the
   to-review (warm-red) and reviewed (green) pills without color clash. */
.prism-badge-pending {
    background: rgba(245, 158, 11, 0.2);
    color: #f59e0b;
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

.white-content .prism-badge-pending {
    background: rgba(245, 158, 11, 0.15);
    color: #d97706;
}

.prism-badge-danger {background: var(--prism-danger-wash);
    color: var(--prism-danger);
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.8em;
    font-weight: 600;
    display: inline-block;
}

.prism-badge-cash {
    background-color: #059437;
    color: #ffffff;
    padding: 5px 12px;
    border-radius: 15px;
    font-size: 0.75em;
    font-weight: 600;
    display: inline-block;
}

.prism-badge-split {
    background: linear-gradient(135deg, var(--prism-accent2) 0%, var(--prism-accent3) 100%);
    color: #ffffff;
    padding: 3px 8px;
    border-radius: 10px;
    font-size: 0.7em;
    font-weight: 600;
    display: inline-block;
    margin-left: 6px;
    vertical-align: middle;
}

/* ===== SOLD ASSET ROWS =====

   assets.html painted sold rows `background: #c12f2f` inline. Three problems,
   only the last of which was visible: it is opaque, so it punched a hole in the
   glass ground the rest of the page composites over; it follows no mode, so the
   same red sat under light mode's dark table text at ~2:1; and colour was the
   ONLY cue, so the state did not survive a greyscale render or a reader who
   cannot see it.

   The replacement is what the rest of the app does with state — a wash rather
   than a fill, plus a form cue. The wash is --prism-danger-wash, the alpha the
   danger badge already ships, so the row tints without becoming a surface:
   --prism-text-2 (what `body .table > tbody > tr > td` sets) measures 6.6-8.9:1
   on it in dark and 5.1-5.9:1 in light, across the mesh's range. The stripe and
   the pill carry the state for anyone the hue does not reach.

   The hue is deliberately unchanged. Red for sold is this app's existing
   convention and the complaint was legibility, not colour. */
.table tr[data-sold="true"] {
    background: var(--prism-danger-wash);
}

/* Leading-edge stripe. On the first cell, not the row: reboot sets
   `table { border-collapse: collapse }` app-wide, and under the collapsed
   border model a `tr` has no border box of its own to hang a border or a
   box-shadow on — backgrounds paint from the row, decoration does not.
   Measures 4.5-6.2:1 against the card; WCAG 1.4.11 wants 3:1 for a non-text
   cue. */
.table tr[data-sold="true"] td:first-child {
    box-shadow: inset 3px 0 0 var(--prism-danger);
}

/* The one solid-fill pill in the badge family, because it sits ON the danger
   wash: give it the wash background the others use and the label lands at
   2.96:1 (the two alphas stack). Filled, the label is --prism-on-semantic —
   #11121c over dark's luminous pink at 6.36:1, #ffffff over light's deep one at
   7.86:1 — which is also why this needs no .white-content override. */
.prism-badge-sold {
    background: var(--prism-danger);
    color: var(--prism-on-semantic);
    padding: 3px 10px;
    border-radius: 10px;
    font-size: 0.7em;
    font-weight: 700;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    display: inline-block;
    margin-left: 6px;
    vertical-align: middle;
}

/* Split Transaction Rows */
.split-child-row {
    background-color: rgba(168, 85, 247, 0.05) !important;
}

.split-child-row td:first-child {
    padding-left: 40px !important;
    position: relative;
}

.split-child-row td:first-child::before {
    content: '└─';
    position: absolute;
    left: 20px;
    color: var(--prism-accent3);
    font-weight: bold;
}

.white-content .split-child-row {
    background-color: rgba(168, 85, 247, 0.08) !important;
}

.split-allocation-label {background: var(--prism-accent2-wash);
    color: var(--prism-accent3);
    padding: 2px 6px;
    border-radius: 8px;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    display: inline-block;
    margin-right: 6px;
}

.white-content .split-allocation-label {
    color: #7b2cbf;
}

.split-allocation-item {
    transition: background-color 0.2s ease;
}

.split-allocation-item:hover {
    background: rgba(168, 85, 247, 0.1) !important;
}

/* ===== DIVIDERS ===== */
.prism-divider {
    text-align: center;
    position: relative;
    margin: 25px 0;
}

.prism-divider::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 1px;background: var(--prism-surface-hover);
}

.prism-divider span {
    background: var(--prism-bg-base);
    padding: 5px 15px;
    position: relative;
    z-index: 1;
    color: var(--prism-warning);
    font-weight: 600;
    font-size: 0.9em;
    border-radius: 20px;
    border: 1px solid #ff8d72;
}

.white-content .prism-divider span {
    background: #e8eaef;
}

/* ===== AND BADGE ===== */
.prism-and-badge {
    background: var(--prism-success);
    color: var(--prism-on-semantic);
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 0.7em;
    font-weight: 600;
    margin: 0 5px;
    display: inline-block;
}

/* ===== INPUT STYLES ===== */
.prism-input {
    font-size: 1.1em;
    font-weight: 500;
    border: none;border-bottom: 2px solid var(--prism-accent-border);
    background: transparent;
    padding: 10px 0;
    width: 100%;color: var(--prism-text);
    transition: all 0.3s ease;
}

.prism-input:focus {
    outline: none;
    border-bottom-color: var(--prism-accent);
    box-shadow: 0 3px 10px rgba(var(--prism-accent-rgb), 0.2);
}

.prism-input::placeholder {color: var(--prism-text-faint);
}

/* ===== SEARCH INPUT WITH FULL BORDER ===== */
.prism-search-input {
    font-size: 1.05em;
    font-weight: 500;border: 2px solid var(--prism-accent-border);background: var(--prism-tint-1);
    padding: 12px 20px;
    border-radius: var(--prism-radius-pill);
    width: 100%;color: var(--prism-text);
    transition: all 0.3s ease;
}

.prism-search-input:focus {
    outline: none;
    border-color: var(--prism-accent);background: var(--prism-tint-2);
    box-shadow: 0 5px 20px rgba(var(--prism-accent-rgb), 0.25);
}

.prism-search-input::placeholder {color: var(--prism-text-dim);
}

.white-content .prism-search-input:focus {
    box-shadow: 0 3px 15px rgba(var(--prism-accent-rgb), 0.15);
}

/* ===== FORM FIELD VOCABULARY =====
   Used in: add_asset.html, edit_asset.html (through macros/asset_fields.html),
   and EVERY form right_sidebar.js renders -- the loan-event wizard, the
   category form, add-cash-transaction, upload-document and transaction detail.

   This exists to replace `.form__label` on the surfaces that carry the most of
   them. That class is black-dashboard's (line 20088): `color: #7676768a`, a
   fixed vendor grey at 54% alpha on 11px uppercase text, which does not flip
   with mode at all. The drawer patched its own copy at
   `#rightSidebar .form__label` and measured the vendor value at 2.06:1 dark /
   1.95:1 light against a 4.5 floor -- roughly half. The two asset forms held
   153 of the app's 261 uses, so converting them was most of the problem and
   left the remaining nine templates for their own change, as that comment
   asked.

   The drawer's 38 have since followed, which is what let both that patch and
   the drawer-only `.sidebar-field-label` third spelling be deleted -- see the
   note where they used to live, further down this file. So a label rendered
   into the right drawer is this class, and there is no second answer.

   --prism-text-2 rather than --prism-text-muted for the label: a field label is
   read, not glanced at, and it is the only thing naming the input beneath it.
   Muted is right for the hint below, which restates something the label already
   said. */
.prism-field {
    margin-bottom: 20px;
}

.prism-field-label {
    display: block;
    margin-bottom: 7px;
    font-size: var(--prism-fs-xs);
    font-weight: 700;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--prism-text-2);
}

/* The required marker. A span rather than a bare `*` so the step controller can
   hide it when a field stops being required -- which happens on every address
   field the moment the asset type is Business. */
.prism-field-req {
    margin-left: 3px;
    color: var(--prism-danger);
}

.prism-field-hint {
    display: block;
    margin-top: 6px;
    font-size: var(--prism-fs-xs);
    line-height: 1.45;
    color: var(--prism-text-muted);
}

/* Unit adornment -- the $ on a money field, the % on a rate, the yrs on a term.
   Drawn inside the control rather than beside it.

   NOT Bootstrap's .input-group. That was the first attempt, on the reasoning
   that the vendor-decoupling block at the bottom of this file already tokenises
   .input-group-text so it would cost no new CSS. It renders wrong here for two
   independent reasons: the vendor sets a background on .input-group-text that
   the decoupling block never overrode, so the addon came out as a solid white
   blob with white text inside it; and .form-control in this app is a 25px pill,
   so the addon could not sit flush against it either -- the $ read as a separate
   round badge floating to the left of the field.

   The spinner goes with it. A number input's stepper arrows overlap a right-hand
   suffix, and nobody increments a purchase price one dollar at a time. */
.prism-field-unit {
    position: relative;
    display: block;
}

.prism-field-unit > .form-control {
    padding-left: 32px;
}

.prism-field-unit--suffix > .form-control {
    padding-left: 18px;
    padding-right: 52px;
}

.prism-field-unit > .form-control::-webkit-outer-spin-button,
.prism-field-unit > .form-control::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.prism-field-unit > input[type="number"].form-control {
    -moz-appearance: textfield;
    appearance: textfield;
}

.prism-field-unit-prefix,
.prism-field-unit-suffix {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    font-size: var(--prism-fs-sm);
    font-weight: 600;
    color: var(--prism-text-muted);
    pointer-events: none;
}

.prism-field-unit-prefix { left: 18px; }
.prism-field-unit-suffix { right: 18px; }

/* A collapsed explanation under a field, for the cases a computed default gets
   wrong. Closed by default: it is reassurance for the minority who need it, not
   something to make everyone read past. */
.prism-field-note {
    margin-top: 8px;
    font-size: var(--prism-fs-xs);
    line-height: 1.5;
    color: var(--prism-text-muted);
}

.prism-field-note summary {
    cursor: pointer;
    list-style: none;
    padding: 2px 0;
    font-weight: 600;
    color: var(--prism-accent-text);
}

.prism-field-note summary::-webkit-details-marker { display: none; }

.prism-field-note summary::before {
    content: '▸ ';
    display: inline-block;
    transition: transform 0.15s ease;
}

.prism-field-note[open] summary::before { content: '▾ '; }

.prism-field-note ul {
    margin: 6px 0 0;
    padding-left: 16px;
}

.prism-field-note li { margin-bottom: 3px; }

/* A figure the form shows but never asks for -- the amortized loan balance.
   Deliberately not a disabled input: a greyed-out box invites the user to try to
   type in it and then wonder why they cannot. This reads as what it is, a
   number the app worked out. */
.prism-static-value {
    padding: 10px 0 2px;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--prism-text);
}

/* A computed value shown beside an input -- down-payment %, equity, LTV. Styled
   as text, not as a disabled input, because it is not a control: it carries no
   `name` and never posts. Both asset routes hand `request.form.to_dict()`
   straight to the model, so an input name that is not an Asset field raises
   FieldDoesNotExist rather than being ignored. */
.prism-field-readout {
    display: block;
    margin-top: 6px;
    font-size: var(--prism-fs-xs);
    font-weight: 600;
    line-height: 1.45;
    color: var(--prism-accent-text);
}

.prism-field-readout:empty {
    display: none;
}

/* An input whose value this form filled in. Kept legible rather than greyed
   out: it is a real value that will be saved, and the user may edit it -- at
   which point the JS drops the attribute and stops recomputing. */
.prism-field-derived {
    border-style: dashed !important;
}

.prism-field-grid {
    display: grid;
    gap: 0 20px;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

.prism-field-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.prism-field-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.prism-field-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 768px) {
    .prism-field-grid,
    .prism-field-grid--2,
    .prism-field-grid--3,
    .prism-field-grid--4 {
        grid-template-columns: 1fr;
    }
}

/* Page-level validation banner. The drawer's `.sidebar-form-error` is the same
   object one surface over; this is the version for a form on the page, which
   needs the icon row and cannot borrow a class scoped to the drawer. */
.prism-form-error {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-bottom: 20px;
    padding: 13px 16px;
    background: var(--prism-danger-wash);
    border: 1px solid var(--prism-danger);
    border-radius: var(--prism-radius-panel);
    color: var(--prism-danger);
    font-size: 0.9rem;
    line-height: 1.5;
}

.prism-form-error[hidden] {
    display: none;
}

/* Group heading inside a step -- "At purchase", "Today". Lighter than a card
   header because it divides one panel rather than starting a new surface. */
.prism-field-group {
    margin: 4px 0 18px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--prism-border);
    font-size: var(--prism-fs-xs);
    font-weight: 700;
    letter-spacing: 0.8px;
    text-transform: uppercase;
    color: var(--prism-text-muted);
}

.prism-field-group:not(:first-child) {
    margin-top: 26px;
}

/* ===== CHECKBOX =====
   Used in: add_asset.html and edit_asset.html (sold, cash purchase, NNN lease),
   through macros/asset_fields.html.

   Was `.sold-checkbox`, page-local in edit_asset.html and the app's only custom
   checkbox; the other two were bare inputs carrying
   `style="width:16px;height:16px;cursor:pointer"`, so three checkboxes on two
   pages were three different controls. Neutral at rest, accent on hover and on
   :checked, per the rule the rest of this file follows.

   --prism-text-dim for the ring, not --prism-border-strong: a 20px box is
   identified almost entirely by its border, because the input fill lands at
   only 1.2-1.3:1 on the page ground in both modes. border-strong is right for a
   .form-control -- a large box with a placeholder in it -- but here it measured
   2.2:1 dark / 1.4:1 light, under the 3:1 WCAG 1.4.11 wants for a control
   boundary. Dim measures 6.0:1 dark and 3.8:1 light. */
.prism-checkbox {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    flex: none;
    width: 20px;
    height: 20px;
    border: 2px solid var(--prism-text-dim);
    border-radius: 4px;
    background-color: var(--prism-input-bg);
    cursor: pointer;
    position: relative;
    transition: background-color 0.2s ease, border-color 0.2s ease;
}

/* Both borders are --prism-accent-text rather than the raw accent, for the same
   reason the choice card's is: the raw accent is one fixed hex in both modes, so
   on a LIGHT ground it collapses to 1.42:1 (teal) and 1.60:1 (amber) against the
   page. On hover that was actively backwards -- the resting ring is 3.84:1, so
   pointing at the control made it HARDER to see. Range is now 5.67-11.82:1
   across all nine skins in both modes.

   The checked FILL stays the raw accent on purpose: it is what the tick is
   measured against, and --prism-on-accent clears 4.44:1 on it. Moving the fill
   would move that floor for no gain, since the ring is what has to separate the
   control from the page. */
.prism-checkbox:hover {
    background-color: var(--prism-tint-4);
    border-color: var(--prism-accent-text);
}

.prism-checkbox:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

.prism-checkbox:checked {
    background-color: var(--prism-accent);
    border-color: var(--prism-accent-text);
}

.prism-checkbox:checked::after {
    content: '✓';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    /* Not white: the fill follows the accent, and white on the bright skins
       measures 1.9:1 (teal), 2.2:1 (amber), 2.4:1 (green) and 2.9:1 (orange).
       This tick is the only thing that says "checked", so it has to clear 3:1 on
       all nine. See --prism-on-accent above for why it does not flip. */
    color: var(--prism-on-accent);
    font-size: 14px;
    font-weight: bold;
    line-height: 1;
}

.prism-check-label {
    display: flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    font-size: 0.95rem;
    font-weight: 500;
    color: var(--prism-text-2);
}

/* ===== CHOICE CARD =====
   Used in: add_asset.html (asset type) and right_sidebar.js (loan-event type).

   Was `.asset-type-card`, page-local in add_asset.html, alongside a second,
   smaller copy as `.lew-type-card` in the drawer and a third, never-used one as
   `.sidebar-category-card`. One component picking one of a few labelled options,
   written three times.

   Colour comes from tokens: the cards are NEUTRAL at rest and spend the accent
   on hover and on the selected card. They used to be an
   rgba(29, 140, 248, ...) wash inside an rgba(29, 140, 248, ...) border -- the
   default skin's accent written as a literal -- so on the other eight skins the
   picker stayed blue while the page around it moved. */
.prism-choice-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    justify-content: center;
}

/* Vertical variant for the drawer, which is too narrow for three across. */
.prism-choice-grid--stack {
    flex-direction: column;
    flex-wrap: nowrap;
}

.prism-choice-card {
    flex: 1 1 220px;
    max-width: 280px;
    cursor: pointer;
    border-radius: 14px;
    padding: 22px 18px;
    text-align: center;
    border: 2px solid var(--prism-border);
    background: var(--prism-surface-2);
    box-shadow: 0 2px 8px var(--prism-shadow-soft);
    transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
    position: relative;
    overflow: hidden;
    user-select: none;
}

.prism-choice-grid--stack .prism-choice-card {
    flex: none;
    max-width: none;
    padding: 14px 16px;
    text-align: left;
}

.prism-choice-card::before {
    content: '';
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at top right, rgba(var(--prism-accent2-rgb), 0.07) 0%, transparent 65%);
    opacity: 0;
    transition: opacity 0.25s;
    pointer-events: none;
}

.prism-choice-card:hover::before {
    opacity: 1;
}

.prism-choice-card:hover {
    border-color: var(--prism-accent-border);
    transform: translateY(-3px);
    box-shadow: 0 8px 28px rgba(var(--prism-accent-rgb), 0.15);
}

.prism-choice-card:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

/* The signup portfolio picker builds each card as a <label> around a real
   checkbox, so click, keyboard and form submission are all native rather than
   re-implemented on a div with role="button". The input has to stay in the
   accessibility tree -- display:none would remove it and take the label's
   semantics with it -- so it is positioned out of sight instead. :focus-within
   carries the focus ring across from the input the card is hiding. */
.prism-choice-card > input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}

.prism-choice-card:focus-within {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

/* --prism-accent-text, not --prism-accent: this border and the dot below are
   what say "selected", and the raw accent is a fixed hex in both modes, so on a
   LIGHT ground the bright skins wash out -- measured 1.42:1 on teal and 1.60:1
   on amber against the page, with the tinted fill only 1.02:1 off a neutral card
   and the dot equally faint, so all three cues missed the 3:1 WCAG 1.4.11 wants.
   (The blue this replaced was no better: it was 2.58:1 on every skin, failing
   quietly because it failed uniformly.) accent-text is the ground-aware accent --
   toward white in dark, toward black in light -- and lands 5.66-12.69:1 across
   all nine skins in both modes. The glows below stay on the raw accent: they are
   decoration, not the cue. */
.prism-choice-card.is-selected {
    border-color: var(--prism-accent-text);
    background: linear-gradient(180deg, var(--prism-tint-4) 0%, var(--prism-tint-2) 100%);
    box-shadow: 0 0 0 1px rgba(var(--prism-accent-rgb), 0.35), 0 6px 24px rgba(var(--prism-accent-rgb), 0.2);
    transform: translateY(-3px);
}

.prism-choice-card.is-selected::after {
    content: '';
    position: absolute;
    top: 10px;
    right: 12px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--prism-accent-text);
    box-shadow: 0 0 8px rgba(var(--prism-accent-rgb), 0.8);
}

.prism-choice-icon {
    font-size: 2rem;
    margin-bottom: 10px;
    display: block;
    opacity: 0.75;
    transition: opacity 0.2s;
}

.prism-choice-grid--stack .prism-choice-icon {
    display: inline-block;
    margin: 0 8px 0 0;
    font-size: 1.35rem;
    vertical-align: -2px;
}

.prism-choice-card.is-selected .prism-choice-icon,
.prism-choice-card:hover .prism-choice-icon {
    opacity: 1;
}

.prism-choice-title {
    font-weight: 700;
    font-size: 0.95rem;
    letter-spacing: 0.3px;
    color: var(--prism-text);
    margin-bottom: 4px;
}

.prism-choice-desc {
    font-size: 0.78rem;
    color: var(--prism-text-muted);
    line-height: 1.35;
}

/* ===== FORM WIZARD =====
   Used in: add_asset.html and edit_asset.html.

   Both pages were one scroll of five stacked card headers with up to eight
   fields in a row. The rail is orientation, not a gate: every step is reachable
   from every other, because on the edit page the user arrived to change one
   thing and already knows which.

   The connector is drawn by each step's ::before, from the previous step's
   centre to its own, which is why the steps must be equal width (`flex: 1 1 0`).
   It stops 26px short at each end so it does not run under a numeral -- the
   numerals sit on a translucent token surface, so a line behind one would show
   through it. */
.prism-wizard-rail {
    display: flex;
    align-items: flex-start;
    margin: 0 0 34px;
    padding: 0;
    list-style: none;
}

.prism-wizard-step {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 9px;
    position: relative;
    padding: 0 4px;
    background: none;
    border: 0;
    font-family: inherit;
    cursor: pointer;
    color: var(--prism-text-muted);
    transition: color 0.2s ease;
}

.prism-wizard-step::before {
    content: '';
    position: absolute;
    top: 16px;
    left: calc(-50% + 26px);
    right: calc(50% + 26px);
    height: 2px;
    border-radius: 2px;
    background: var(--prism-border);
    transition: background 0.2s ease;
}

.prism-wizard-step:first-child::before {
    display: none;
}

.prism-wizard-step.is-active::before,
.prism-wizard-step.is-done::before {
    background: var(--prism-accent-border);
}

.prism-wizard-num {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.9rem;
    font-weight: 700;
    border: 2px solid var(--prism-border);
    background: var(--prism-surface-2);
    color: var(--prism-text-muted);
    transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}

.prism-wizard-label {
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.3px;
    text-align: center;
    line-height: 1.3;
}

.prism-wizard-step:hover {
    color: var(--prism-text);
}

.prism-wizard-step:hover .prism-wizard-num {
    border-color: var(--prism-accent-border);
    color: var(--prism-text);
}

.prism-wizard-step:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 3px;
    border-radius: 8px;
}

/* Same reasoning as the choice card: the state cue is --prism-accent-text, and
   the glow around it is the raw accent because a glow is decoration. */
.prism-wizard-step.is-active {
    color: var(--prism-text);
}

.prism-wizard-step.is-active .prism-wizard-num {
    border-color: var(--prism-accent-text);
    background: var(--prism-tint-4);
    color: var(--prism-accent-text);
    box-shadow: 0 0 0 4px rgba(var(--prism-accent-rgb), 0.12);
}

.prism-wizard-step.is-done .prism-wizard-num {
    border-color: var(--prism-accent-border);
    color: var(--prism-accent-text);
}

/* A step holding a field that failed validation. Border only -- the numeral is
   3 characters wide and a danger fill behind it would have to carry its own
   contrast floor for the digit. */
.prism-wizard-step.is-invalid .prism-wizard-num {
    border-color: var(--prism-danger);
    color: var(--prism-danger);
    background: var(--prism-danger-wash);
}

.prism-wizard-panel {
    display: none;
}

.prism-wizard-panel.is-active {
    display: block;
    animation: prismWizardIn 0.28s ease both;
}

@keyframes prismWizardIn {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

.prism-wizard-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 30px;
    padding-top: 22px;
    border-top: 1px solid var(--prism-border);
}

/* Puts the FIRST button in the markup on the right. Used where the primary
   action has to win implicit submission -- pressing Enter in a text field
   activates the first submit button in tree order, so a destructive button
   written first will run on a keystroke. The safe action goes first in the DOM
   and this puts it back where it belongs on screen. */
.prism-wizard-actions--reverse {
    flex-direction: row-reverse;
}

@media (prefers-reduced-motion: reduce) {
    .prism-wizard-panel.is-active { animation: none; }
}

@media (max-width: 768px) {
    .prism-wizard-label { font-size: 0.68rem; }
    .prism-wizard-step::before { left: calc(-50% + 22px); right: calc(50% + 22px); }
}

/* ===== LOAN EVENT TIMELINE ROW =====
   Used in: add_asset.html and edit_asset.html, seeded server-side by
   macros/asset_fields.html and re-rendered client-side by each page's
   LoanEvents controller.

   Read-only on the page: both add and edit hand editing to the drawer wizard,
   so a row shows what is staged and offers Edit / Remove. It replaced two
   near-identical sets of three <template> blocks -- ~340 lines in add_asset.html
   and ~290 in edit_asset.html -- that each rendered a full editable form per
   event, so a stack of three events was three stacked forms. */
.loan-event-row {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
    padding: 12px 16px;
}

.loan-event-row[hidden] {
    display: none;
}

.loan-event-row-main {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    flex: 1;
    min-width: 0;
}

.loan-event-row-date {
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--prism-text-2);
    white-space: nowrap;
}

.loan-event-row-summary {
    font-size: 0.82rem;
    color: var(--prism-text-muted);
}

.loan-event-row-actions {
    display: flex;
    gap: 4px;
    flex: none;
}

/* ===== HIGHLIGHT BOX ===== */
.prism-highlight-box {background: var(--prism-success-wash);
    border: 1px solid rgba(0, 242, 195, 0.3);
    border-radius: 10px;
    padding: 20px;
    transition: all 0.3s ease;
}

.prism-highlight-box:hover {background: var(--prism-success-wash);
    border-color: rgba(0, 242, 195, 0.4);
}

.white-content .prism-highlight-box {
    border: 1px solid rgba(0, 242, 195, 0.25);
}

.white-content .prism-highlight-box:hover {
    border-color: rgba(0, 242, 195, 0.35);
}

/* ===== EMOJI ANIMATION ===== */
.prism-emoji-bounce {
    display: inline-block;
    animation: prism-bounce 2s ease-in-out infinite;
}

@keyframes prism-bounce {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-8px); }
}

/* ===== WELCOME TEXT ANIMATION ===== */
.prism-welcome-animate {
    animation: prism-slide-fade-in 1s ease-out;
}

@keyframes prism-slide-fade-in {
    0% {
        opacity: 0;
        transform: translateY(-30px);
        transform: translateX(-30px);
        transform: rotate(30deg);
        transform: scale(0.5);
    }
    100% {
        opacity: 1;
        transform: translateY(0);
        transform: translateX(0);
        transform: rotate(0deg);
        transform: scale(1);
    }
}

/* ===== GLOW EFFECT FOR ICONS ===== */
.prism-icon-glow {
    filter: drop-shadow(0 0 10px rgba(var(--prism-accent-rgb), 0.5));
    transition: filter 0.3s ease;
}

.prism-icon-glow:hover {
    filter: drop-shadow(0 0 15px rgba(var(--prism-accent-rgb), 0.8));
}

.white-content .prism-icon-glow {
    filter: drop-shadow(0 0 8px rgba(var(--prism-accent-rgb), 0.3));
}

.white-content .prism-icon-glow:hover {
    filter: drop-shadow(0 0 12px rgba(var(--prism-accent-rgb), 0.5));
}

/* ===== CONDITION/GROUP STYLES ===== */
.prism-condition-row {background: var(--prism-surface-2);
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 10px;border: 1px solid var(--prism-border);
    transition: all 0.2s ease;
}

.prism-condition-row:hover {
    border-color: rgba(var(--prism-accent-rgb), 0.3);background: var(--prism-surface-hover);
}

.prism-condition-group {border: 2px dashed var(--prism-accent-border);
    border-radius: 10px;
    padding: 15px;
    margin-bottom: 15px;background: var(--prism-tint-1);
    transition: all 0.2s ease;
}

.prism-condition-group:hover {background: var(--prism-tint-2);border-color: var(--prism-accent-border);
}

/* ===== ARROW INDICATOR ===== */
.prism-arrow {
    font-size: 1.5em;color: var(--prism-success);
    margin: 0 15px;
    display: inline-block;
}

/* ===== UTILITIES ===== */
.prism-text-muted {color: var(--prism-text-muted) !important;
}

.prism-text-primary {
    color: var(--prism-accent-text) !important;
}

/* Re-skin black-dashboard's .text-info utility — semantically it is "the accent
   color", so it should track the active skin rather than stay hardcoded blue.
   prism-common.css loads after black-dashboard.css with equal specificity +
   !important, so this wins. Applies app-wide (section headers, labels, etc.). */
.text-info {
    color: var(--prism-accent-text) !important;
}

a.text-info:hover,
a.text-info:focus {
    color: var(--prism-accent-light) !important;
}

.prism-text-success {color: var(--prism-success) !important;
}

.prism-text-warning {
    color: var(--prism-warning) !important;
}

.prism-text-danger {
    color: var(--prism-danger) !important;
}

.prism-text-secondary {
    color: var(--prism-text-muted) !important;
}

/* ===== SCROLLBAR STYLING (optional enhancement) ===== */
.prism-scrollbar::-webkit-scrollbar {
    width: 8px;
    /* `width` styles the vertical bar only; without a height the HORIZONTAL
       bar in the same region falls back to the platform's own, which on
       Windows and Linux is a grey slab straight across a rounded card. */
    height: 8px;
}

.prism-scrollbar::-webkit-scrollbar-track {background: var(--prism-surface-2);
    border-radius: 10px;
}

.prism-scrollbar::-webkit-scrollbar-thumb {background: var(--prism-fill-1);
    border-radius: 10px;
}

.prism-scrollbar::-webkit-scrollbar-thumb:hover {background: var(--prism-fill-2);
}

/* `.alert-info` used to need a light-mode override here, restating the accent
   wash and a literal `#222a42`. It does not any more: the base rule in the
   VENDOR SURFACE DECOUPLING section reads --prism-tint-3 / --prism-accent-border
   / --prism-text, all of which already flip. Two overrides retired. */

/* ============================================================================
   RIGHT SIDEBAR STYLES
   Used in: includes/right_sidebar.html (included once, globally, by
   layouts/base.html) — driven entirely by assets/js/right_sidebar.js.

   The drawer has its OWN surface, not the page's. Where the page is a line
   grid over a three-point radial mesh, the drawer is a dot matrix lit by a
   single bloom at its own top-right corner. Same family, different
   vocabulary — which is the point: it should read as related to the main
   panel, not as a slice of it.

   It is opaque, so nothing behind shows through and there is no
   backdrop-filter at any width. That is deliberate: a blurred backdrop
   destroys fine texture (a 46px grid vanishes under blur(18px)), and it is
   what previously forced phone width to opt out of the treatment entirely.

   Mode-agnostic throughout: every colour is a token, so light mode is the
   usual token flip with no `.white-content` override — see the token contract
   at the top of this file. The one exception is .sidebar-map-frame, whose
   override inverts a filter rather than restating a colour.
   ============================================================================ */

/* Sidebar Container */
.right-sidebar {
    position: fixed;
    top: 0;
    right: calc(-1 * var(--rsb-w)); /* Hidden by default — fully off-screen regardless of clamped width */
    width: var(--rsb-w);
    height: 100vh;

    /* Dot matrix over a corner bloom over the drawer's own ground.
       background-size is per-layer and positional, so the two lists stay in
       step: dots tile at 22px, the bloom and the ground do not tile at all. */
    background-color: var(--prism-drawer-ground);
    background-image:
        radial-gradient(circle, var(--prism-drawer-dot) 1.1px, transparent 1.2px),
        radial-gradient(85% 26% at 100% 0%,
            rgba(var(--prism-accent-rgb), var(--prism-drawer-bloom)) 0%,
            rgba(var(--prism-accent3-rgb), calc(var(--prism-drawer-bloom) * 0.45)) 45%,
            transparent 72%);
    background-size: 22px 22px, auto;
    background-repeat: repeat, no-repeat;

    border-left: 1px solid var(--prism-accent-border);

    /* Two stacked shadows off ONE token rather than a second, heavier token:
       layering roughly doubles the perceived depth while both layers still
       flip with the mode. The old -5px 0 40px rgba(0,0,0,0.6) was a dark-mode
       literal that read as a smudge over the light ground. The inset rim is
       what makes a translucent edge read as an edge rather than a fade. */
    box-shadow: -28px 0 70px -20px var(--prism-shadow-soft),
                -8px  0 24px  -8px var(--prism-shadow-soft),
                inset 1px 0 0 var(--prism-border-faint);
    transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
    z-index: 1051;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.right-sidebar.active {
    right: 0;
}

/* ---------------------------------------------------------------------------
   Drag-to-resize handle

   The width token targets a proportion, and no single proportion suits a
   1280px laptop and a 3440px ultrawide -- so the default gets better and the
   reader gets the last word. right_sidebar.js writes the dragged width to
   --rsb-pref (never --rsb-w; see the token) and persists it, so every consumer
   of the token follows for free: the drawer, the content padding, the scroll
   rail, the toast stack.

   Inside the drawer, because .right-sidebar is `overflow: hidden` -- a handle
   offset out onto the page would simply be clipped away. Absolutely
   positioned, so it is out of the drawer's flex column and the header and
   .sidebar-content { flex: 1 } are untouched. It lands over .sidebar-content's
   30px left padding, where there is nothing to intercept.
   --------------------------------------------------------------------------- */
.right-sidebar-resize {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 8px;
    /* Above the drawer's own positioned content -- renderSpendingChart sets
       `position: relative` on its chart containers and .category-sticky-bar is
       z-index 6, both of which would otherwise paint over the handle since it
       is the drawer's FIRST child and would tie on z-index: auto. Well under
       .cat-form-popover's 1060, which must stay on top. */
    z-index: 20;
    cursor: ew-resize;
    /* A drag here must not also scroll the drawer or pan the page. */
    touch-action: none;
    background: transparent;
}

/* The grab target is 8px and the mark is 2px, deliberately: a hit area you can
   actually hit is wider than a line you want to look at. Drawn on ::after so
   the target itself stays fully transparent over the drawer's own material. */
.right-sidebar-resize::after {
    content: '';
    position: absolute;
    left: 3px;
    top: 0;
    bottom: 0;
    width: 2px;
    background: transparent;
    transition: background 0.15s ease;
}

.right-sidebar-resize:hover::after,
.right-sidebar-resize:focus-visible::after,
body.rsb-resizing .right-sidebar-resize::after {
    background: var(--prism-accent);
}

/* The hairline above IS the focus ring -- a real outline on an 8px sliver
   spanning the full viewport height reads as a border on the drawer rather
   than as focus. It is full-height and accent-coloured, so it is not a
   quieter indicator than the default, just a differently shaped one. */
.right-sidebar-resize:focus-visible {
    outline: none;
}

/* While dragging, everything that tracks the drawer's edge has to move WITH
   the pointer rather than ease toward it -- all five together.

   This is test_reduced_motion_does_not_desync_the_pair's invariant seen from
   the other side: .prism-scroll-progress insets by var(--rsb-w) and outranks
   the drawer (1055 vs 1051), so killing the drawer's transition while the rail
   keeps its 0.4s ease paints the rail across the drawer for the whole drag --
   the exact artefact that inset exists to prevent, once per pointermove. */
body.rsb-resizing {
    cursor: ew-resize;
    user-select: none;
}

/* Sweeping the pointer across the drawer during a drag should not light up
   every row it passes over, or start selecting their text. Pointer capture
   keeps the events coming to the handle; this stops the content reacting to
   the pointer being over it. */
body.rsb-resizing .sidebar-content {
    pointer-events: none;
}

body.rsb-resizing .right-sidebar,
body.rsb-resizing .main-panel > .content,
body.rsb-resizing .main-panel > footer,
body.rsb-resizing .main-panel > .navbar.navbar-absolute,
body.rsb-resizing .prism-scroll-progress {
    transition: none;
}

/* When the right sidebar is open, reserve space in the main content area
   so the sidebar and main content sit side-by-side instead of overlapping. */
.main-panel > .content,
.main-panel > footer,
.main-panel > .navbar.navbar-absolute {
    transition: padding-right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

body.right-sidebar-open .main-panel > .content,
body.right-sidebar-open .main-panel > footer,
body.right-sidebar-open .main-panel > .navbar.navbar-absolute {
    padding-right: calc(var(--rsb-w) + 30px);
}

/* Below this, overlay instead of splitting -- the same arrangement the <=768px
   block already uses, with the boundary moved from "phone" to "not wide enough
   to split".

   Splitting narrows the CONTENT BOX, and Bootstrap's grid does not know that:
   `col-lg-4` keys off the VIEWPORT, so on a 1300px screen with the drawer open
   the three summary-metric cards still lay out three-across inside ~850px, and
   every col-lg-* grid in the app does the same. That is what reads as "the
   whole page went condensed when I opened the drawer" -- the drawer's width is
   only half of it. Overlaying gives the grid its real width back, and the page
   underneath stays the layout it was designed as.

   1920px, and the number is derived rather than judged. The binding layout is
   the categories ledger, the widest fixed-track grid in the app: 780px of
   fixed columns plus ~310px of sidebar and content padding before its one
   flexible track (the category NAME) gets anything. Its own comment records
   what happens when that track is squeezed -- "at a 1400px viewport the name
   column was 176px, of which .led-pname got 0 and every category on the page
   rendered nameless" -- and its shedding thresholds are viewport queries, so
   they cannot see the drawer at all. Splitting leaves that column:

       1440 -> 114px    1728 -> 90px     1920 -> 224px
       1512 -> 164px    1800 -> 140px    2048 -> 314px

   so every laptop width was already below the width that failure was reported
   at, drawer open, before this change; 1728 is worst because it is the first
   width above the ledger's own 1620px breakpoint, where the full column set
   comes back. Splitting above 1920 keeps that track over 220px at every width.
   tests/capture_screenshots.py:528 already declines to open the drawer on that
   page, which is the same fact recorded as a workaround.

   The practical shape: every laptop and 1920 desktop overlays -- which is what
   Linear, Notion and Gmail all do with a detail panel -- and only a genuinely
   wide monitor splits, where side-by-side is actually worth the width.

   app/home/templates/dashboard.html carries the matching `min-width: 1921px` on
   its own drawer-open rule; keep the two in step. */
@media (max-width: 1920px) {
    body.right-sidebar-open .main-panel > .content,
    body.right-sidebar-open .main-panel > footer,
    body.right-sidebar-open .main-panel > .navbar.navbar-absolute {
        padding-right: 30px;
    }
}

/* Sidebar Header */
.sidebar-header {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    padding: 0 30px;
    /* Sized to the navbar's own rendered height, not to its own content, so
       this border-bottom lands exactly on the navbar's bottom edge — the same
       edge .prism-scroll-progress rides — and the two read as one continuous
       line across the viewport instead of two independently-guessed ones.
       --prism-navbar-h is measured (not hardcoded) by prism-scroll-progress.js
       for the same reason documented there: the navbar is fixed with no
       declared height, so CSS alone cannot find it. The :root fallback (64px)
       only covers the instant before that script's first measurement lands. */
    height: var(--prism-navbar-h);
    /* A floor under the measurement, because the failure is not graceful: a
       navbar height that comes back too small collapses this box and pushes
       the 40px .sidebar-close (plus the 3px border) straight out of it, over
       the drawer content. That shipped twice while the alignment itself
       looked fine on the page it was checked on. 56px clears the control with
       room to spare and sits well under any real navbar, so it never argues
       with the measurement in the normal case — it only catches a bad one.
       Height belongs to the navbar; fitting its own controls belongs here. */
    min-height: 56px;
    box-sizing: border-box;
    border-bottom: 3px solid var(--prism-accent-border);
    /* An accent wash over the drawer's own ground — opaque, so the corner
       bloom is occluded behind the whole header rather than showing through it.

       That last part is structural, not cosmetic. Everything in this header
       carries a colour the header does not control: .sidebar-close's glyph is
       a fixed blue while the bloom is skin-keyed, and .sidebar-title takes an
       arbitrary per-category colour from the database when the drilldown sets
       --cat-color. Both landed on the bloom and both went under their floor —
       the purple #7c4dff category title measured 1.86:1 at the worst skin,
       against 3.14:1 on the pre-Prism header. Since a category colour is user
       data, no amount of per-colour tuning closes that; the ground has to stop
       varying instead. Opaque here puts it at 3.25:1, slightly better than the
       header it replaced, and covers anything added to this bar later.

       The wash itself still follows the skin, which is what let the hardcoded
       #222537→#1a1d2e gradient and its .white-content twin go. */
    background: linear-gradient(180deg, var(--prism-tint-3) 0%, transparent 100%),
                var(--prism-drawer-ground);
    flex-shrink: 0;
    position: relative;
}

.sidebar-title {
    font-size: var(--prism-fs-xl);
    font-weight: 700;
    margin: 0;color: var(--prism-text);
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

/* When the category drilldown sets --cat-color via inline style, swap to a
   category-themed solid color. The attribute selector matches any inline
   style mentioning the custom property, so JS just needs setProperty
   ('--cat-color', '#…') / removeProperty to flip modes. Light-mode selector
   is included so the category color also wins over .white-content's solid
   text override (same specificity, declared after — wouldn't override
   otherwise).

   NOTE: this is one of the two selectors in the drawer that key off the inline
   style ATTRIBUTE rather than a class (the other is the scroll lock below).
   The setProperty call is in right_sidebar.js, in the category/tag drilldown;
   it carries a matching note. Do not "clean up" that inline write into a class
   — this selector cannot see a class. */
.sidebar-title[style*="--cat-color"],
.white-content .sidebar-title[style*="--cat-color"] {
    color: var(--cat-color);
}

/* The cat-icon-ph placeholder is hydrated to either a text emoji or an <img>;
   neither should inherit the parent's gradient text-fill.

   This is the drilldown header — the one place a category's icon is the
   subject rather than a marker beside a row — so it gets a deliberately large
   glyph. Both forms are sized to land at the same 30px in a 64px header, from
   two different mechanisms: a text emoji scales with font-size, an <img> needs
   real dimensions.

   1.5em, off .sidebar-title's --prism-fs-xl (1.25rem = 20px), so the glyph is
   30px and matches the <img> exactly. It read 1.35em while the title was
   1.4em, and that pair only landed on 30px if `body` was 16px — which is true
   of tests/test_category_form_in_a_browser.py's harness and NOT of the app,
   where black-dashboard sets body to 0.875rem. So in the product the emoji
   rendered 26.5px beside a 30px image the whole time, and the test that exists
   to catch exactly that measured a page the app never serves. Both numbers are
   absolute now, so the two forms agree wherever they render. Worth knowing
   before trusting any px measured in that harness.

   THE !important IS LOAD-BEARING. hydrateCategoryIconPlaceholders builds the
   <img> with `img.style.width = '16px'` written straight onto the element, and
   an inline style outranks any stylesheet rule short of !important — so the
   22px this rule used to declare never applied at all, and a company using an
   uploaded emoji-image got a 16px icon beside 22px text while the comment here
   claimed otherwise. The 16px is correct for the transaction rows that helper
   mostly serves; only the title wants more. */
.sidebar-title .cat-icon-ph {
    -webkit-text-fill-color: initial;
    color: initial;
    display: inline-flex;
    align-items: center;
    font-size: 1.5em;
    line-height: 1;
}
.sidebar-title .cat-icon-ph img {
    width: 30px !important;
    height: 30px !important;
}

.sidebar-close {
    /* Its own ground rather than the drawer's, because this control sits at
       the exact centre of the corner bloom — and the bloom is skin-keyed
       (--prism-accent) while the glyph is not (--prism-accent-text is a fixed
       blue in each mode). On the teal skin that puts a blue glyph on a cyan
       wash: measured 3.16:1 at 700px viewport height, 3.07:1 at 1000px and
       2.99:1 at 1800px, i.e. through the 3:1 floor for a control on a tall
       screen. The bloom's vertical extent is a PERCENTAGE of drawer height, so
       the taller the viewport the more of it lands behind this button.

       An opaque backing makes the contrast deterministic and independent of
       both skin and viewport, and gives the drawer's only close affordance a
       hit-surface it did not have. */
    background: var(--prism-drawer-ground);
    border: none;
    color: var(--prism-accent-text);
    font-size: 1.3em;
    cursor: pointer;
    padding: 8px;
    border-radius: 8px;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
}

.sidebar-close:hover {
    /* The tint LAYERS over the opaque ground rather than replacing it. As a
       bare `background: var(--prism-tint-4)` it dropped the backing above and
       re-exposed the corner bloom in the one state where the user is about to
       click: measured 1.98:1 on teal and 2.91:1 on the default blue, worse
       than the un-backed button at rest, because the tint is accent-keyed and
       so is the bloom while the glyph is a fixed blue.

       --prism-tint-4 is a colour, and in the background shorthand a bare
       colour is only legal as the final layer, so it takes the two-stop
       gradient wrapper; --prism-drawer-ground is last, where a colour belongs.
       Worst case across all nine skins is now 4.02:1 (teal, dark). */
    background: linear-gradient(var(--prism-tint-4), var(--prism-tint-4)),
                var(--prism-drawer-ground);
    transform: rotate(90deg);
}

/* Sidebar Content */
.sidebar-content {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 25px 30px;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    /* On glass, a scrollbar gutter appearing/disappearing shifts content
       against a visible ground rather than against a flat fill. */
    scrollbar-gutter: stable;
}

/* Custom scrollbar for sidebar */
.sidebar-content::-webkit-scrollbar,
.sidebar-view-scroll::-webkit-scrollbar {
    width: 8px;
}

.sidebar-content::-webkit-scrollbar-track,
.sidebar-view-scroll::-webkit-scrollbar-track {background: var(--prism-surface-2);
    border-radius: 10px;
}

.sidebar-content::-webkit-scrollbar-thumb,
.sidebar-view-scroll::-webkit-scrollbar-thumb {background: var(--prism-fill-1);
    border-radius: 10px;
}

.sidebar-content::-webkit-scrollbar-thumb:hover,
.sidebar-view-scroll::-webkit-scrollbar-thumb:hover {background: var(--prism-fill-2);
}

/* Highcharts chrome inside the drawer. Moved here from a <style> element that
   right_sidebar.js built at runtime in init() — it was static CSS the whole
   time, and CSS living in a JS string is invisible to every contract test in
   tests/test_light_mode_css.py. prism-highcharts.js already sets
   credits/exporting disabled as the app-wide convention, so this is the
   belt-and-braces half for charts built before a page opts in. */
.highcharts-credits,
.highcharts-exporting-group { display: none !important; }

/* Scroll lock: keep wheel/touch scrolling inside the drawer's scroll regions
   instead of chaining to the page behind it once a list hits its top/bottom.

   .sidebar-scroll-region is the contract; the [style*=] forms are a deliberate
   fallback for any container that gets an inline overflow without the class.
   They are cheap, and they are what this behaviour depended on before the
   class existed. Keep BOTH spellings — right_sidebar.js writes the un-spaced
   `overflow-y:auto` in at least one place (the category transactions list). */
#rightSidebar .sidebar-content,
#rightSidebar .sidebar-scroll-region,
#rightSidebar [style*="overflow-y: auto"],
#rightSidebar [style*="overflow-y:auto"],
#rightSidebar [style*="overflow: auto"],
#rightSidebar [style*="overflow:auto"] { overscroll-behavior: contain; }

/* Paired with the [style*=] selectors above and with the five
   .closest('.sidebar-scroll-region, [style*="overflow-y: auto"]') calls in
   right_sidebar.js, which scroll a validation error back into view. */
.sidebar-scroll-region {
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Sidebar Overlay (optional - currently commented out in HTML) */
.sidebar-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1039;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s ease;
}

.sidebar-overlay.active {
    opacity: 1;
    visibility: visible;
}

/* ===== SIDEBAR CONTENT COMPONENTS ===== */

/* Section Headers within Sidebar */
.sidebar-section-header {
    font-size: var(--prism-fs-lg);
    font-weight: 600;
    color: var(--prism-accent-text);
    margin: 25px 0 15px 0;
    padding-bottom: 10px;border-bottom: 2px solid var(--prism-accent-border);
    display: flex;
    align-items: center;
    gap: 10px;
}

.sidebar-section-header i {
    font-size: 1.2em;
}

/* Transaction Item in Sidebar */
.sidebar-transaction-item {background: var(--prism-surface-hover);border: 1px solid var(--prism-border-strong);
    border-radius: 10px;
    padding: 15px;
    margin-bottom: 12px;
    transition: all 0.3s ease;
    cursor: pointer;
}

.sidebar-transaction-item:hover {background: var(--prism-tint-4);border-color: var(--prism-accent-border);
    transform: translateX(-3px);
}

.sidebar-transaction-date {
    font-size: var(--prism-fs-xs);color: var(--prism-text-muted);
    margin-bottom: 5px;
}

.sidebar-transaction-merchant {
    font-weight: 600;
    font-size: var(--prism-fs-base);
    margin-bottom: 5px;color: var(--prism-text);
}

.sidebar-transaction-amount {
    font-size: var(--prism-fs-lg);
    font-weight: 700;color: var(--prism-success);
}

.sidebar-transaction-amount.negative {
    color: var(--prism-danger);
}

/* Stats Summary in Sidebar */
.sidebar-stats-summary {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 15px;
    margin-bottom: 25px;
}

/* Inside the drawer, but still a surface at rest, so it follows the same rule
   as a card on the page. The drawer's own identity — its opaque ground, its dot
   matrix and the accent bloom in .sidebar-header — is untouched; the bloom is
   what carries the skin in here, and it did not need this panel's help. */
.sidebar-stat-card {background: var(--prism-veil-3);border: 1px solid var(--prism-border);
    border-radius: 10px;
    padding: 15px;
    text-align: center;
    transition: all 0.3s ease;
}

.sidebar-stat-card:hover {
    border-color: rgba(var(--prism-accent-rgb), 0.5);
    transform: translateY(-2px);
    background: rgba(var(--prism-accent-rgb), 0.15);
}

.sidebar-stat-label {
    font-size: var(--prism-fs-xs);color: var(--prism-text-muted);
    margin-bottom: 5px;
}

.sidebar-stat-value {
    font-size: var(--prism-fs-xl);
    font-weight: 700;
    color: var(--prism-accent-text);
}

/* Key-value detail rows — asset / property detail panels */
.sidebar-kv-group { margin-bottom: 4px; }
.sidebar-kv-row {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding: 7px 0;border-bottom: 1px solid var(--prism-border-faint);
    gap: 16px;
}
.sidebar-kv-row:last-child { border-bottom: none; }
.sidebar-kv-label {
    font-size: var(--prism-fs-sm);
    font-weight: 500;color: var(--prism-text-muted);
    white-space: nowrap;
    flex-shrink: 0;
}
.sidebar-kv-value {
    font-size: var(--prism-fs-base);
    font-weight: 600;color: var(--prism-text);
    text-align: right;
    word-break: break-word;
}

/* FREE TEXT — a note, a description. Anything the user typed into a textarea.

   The two-column row above is right for a scalar and wrong for a paragraph:
   `$211,400.00` wants to sit hard right against its label, and four sentences
   do not. So the label goes on its own line and the text takes the full width,
   left aligned, at the weight of prose rather than of a figure.

   `white-space: pre-wrap` is the half that fixes the actual bug. The plain row
   inherits `normal`, which collapses every run of spaces and every newline the
   author typed — a loan-event note written as four labelled paragraphs rendered
   as one run-on block, while the SAME string in the wizard's <textarea> showed
   correctly, because a textarea is pre-wrap by UA default. So the drawer and
   the editor disagreed about the text they were both showing.

   Safe because the value is escaped: renderPropertyField runs it through
   RightSidebar.escapeHtml before it reaches the DOM, so pre-wrap renders the
   literal newlines and nothing else. It is NOT a licence to inject markup here
   — the alternative fix, .replace(/\n/g, '<br>'), only looks equivalent and
   depends on that escape having already run.

   `word-break: break-word` is inherited from the base rule and matters more
   here: pre-wrap alone will not break a pasted 200-character URL. */
.sidebar-kv-row--stack {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
}
.sidebar-kv-row--stack .sidebar-kv-label {
    white-space: normal;
}
.sidebar-kv-row--stack .sidebar-kv-value {
    text-align: left;
    white-space: pre-wrap;
    font-weight: 500;
    line-height: 1.5;
}

/* Inline tag for derived/calculated values */
.sidebar-derived-tag {
    display: inline-block;
    font-size: var(--prism-fs-2xs);
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 4px;background: var(--prism-tint-3);
    color: var(--prism-accent-text);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.25);
    margin-left: 5px;
    letter-spacing: 0.03em;
    vertical-align: middle;
    text-transform: uppercase;
}

/* Inline map embed in sidebar */
.sidebar-map-wrap {
    margin-top: 14px;
    border-radius: 10px;
    overflow: hidden;
    border: 1px solid rgba(var(--prism-accent-rgb), 0.2);
}
.sidebar-map-frame {
    width: 100%;
    height: 210px;
    border: none;
    display: block;
    filter: saturate(0.85) brightness(0.9);
}
.white-content .sidebar-map-frame { filter: none; }
.sidebar-map-link {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 5px;
    padding: 6px 12px;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.03em;
    color: var(--prism-accent-text);
    text-decoration: none;
    background: rgba(var(--prism-accent-rgb), 0.06);
    border-top: 1px solid rgba(var(--prism-accent-rgb), 0.15);
    transition: background 0.15s, color 0.15s;
}
.sidebar-map-link:hover {
    background: rgba(var(--prism-accent-rgb), 0.12);
    color: var(--prism-accent-text);
    text-decoration: none;
}

/* Loan event block — matches flat KV row style, separated by a hairline */
.sidebar-loan-event {
    padding: 12px 0 4px;border-bottom: 1px solid var(--prism-border-faint);
    margin-bottom: 4px;
}
.sidebar-loan-event:last-child { border-bottom: none; }
.sidebar-loan-event-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
}

/* Empty State */
.sidebar-empty-state {
    text-align: center;
    padding: 60px 20px;color: var(--prism-text-muted);
}

.sidebar-empty-state i {
    font-size: 3em;
    margin-bottom: 20px;
    opacity: 0.5;
}

.sidebar-empty-state p {
    font-size: var(--prism-fs-lg);
    margin: 0;
}

/* Loading State */
.sidebar-loading {
    text-align: center;
    padding: 60px 20px;
}

.sidebar-loading .spinner {
    border: 4px solid rgba(var(--prism-accent-rgb), 0.2);
    border-top: 4px solid var(--prism-accent);
    border-radius: 50%;
    width: 50px;
    height: 50px;
    animation: spin 1s linear infinite;
    margin: 0 auto 20px;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* The inline-spinner utility right_sidebar.js and the exit planner emit
   (`tim-icons icon-refresh-02 spin`). It lived only in index.html's page
   styles, so the icon silently did not rotate anywhere but the dashboard. */
.spin {
    animation: spin 1s linear infinite;
    display: inline-block;
}

/* ============================================================================
   PRISM BUSY PILL (prism-busy.js)
   The shared floating status for any page that recomputes in place: a fixed
   pill under the navbar saying "an answer is on its way" or "it never came".
   Deliberately near-opaque (--prism-overlay): it is chrome floating over
   arbitrary content, the same exemption dropdowns and toasts take from the
   glass-over-ambient rule. First consumer: /exit-planner.
   ========================================================================= */

.prism-busy {
    position: fixed;
    top: 84px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1060;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 18px;
    border-radius: 999px;
    background: var(--prism-overlay);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid var(--prism-accent-border);
    color: var(--prism-text);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    box-shadow: 0 2px 10px var(--prism-shadow-soft);
    animation: prism-busy-in 0.18s ease;
}

/* [hidden] must win over the flex above — UA-level display loses to any
   class selector (see the category form's [hidden] note). */
.prism-busy[hidden] { display: none; }

.prism-busy .tim-icons {
    color: var(--prism-accent);
    font-size: 0.85rem;
}

.prism-busy.is-error {
    border-color: var(--prism-danger);
}

@keyframes prism-busy-in {
    from { opacity: 0; transform: translate(-50%, -8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}

/* ============================================================================
   RIGHT-DRAWER FORM SHELL
   Used in: right_sidebar.js — openAddCashTransaction, openUploadDocument,
   openCategoryForm, openEditTag.

   Six renderers built a byte-identical shell out of inline styles: the same
   flex column eight times, the same error box five times, the same field label
   eleven times. Promoted here because they are one component rendered six
   ways, and because the colours inside them were the file's densest cluster of
   rgba(255,255,255,…) literals — an inline style attribute outranks any
   .white-content rule short of !important, so those literals were not a style
   bug, they were a surface light mode could not reach at all.

   Three of those six were the category forms, which have since collapsed into
   the single openCategoryForm — see CATEGORY FORM (right drawer) below. The
   swatch, icon-tile and segmented-control rules here are shared with it and
   are what stopped that consolidation needing its own copies.

   Layout stays inline on purpose: the theming moved, the margins did not.
   Mode-agnostic throughout — every colour is a token, so light mode is the
   usual token flip with no override.
   ============================================================================ */

.sidebar-form-shell {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* Validation error banner. --prism-danger-wash is stronger than the .1 alpha
   the inline version used (.2 dark / .14 light) — deliberate: the old veil was
   nearly invisible on the light glass drawer. */
.sidebar-form-error {
    display: none;
    margin-bottom: 20px;
    padding: 12px;
    background: var(--prism-danger-wash);
    border: 1px solid var(--prism-danger);
    border-radius: 8px;
    color: var(--prism-danger);
}

/* THE DRAWER'S FIELD LABELS LIVE IN .prism-field-label, above.

   There used to be three of them here. `.form__label` is black-dashboard's
   (line 20088): 11px uppercase at `color: #7676768a`, a fixed vendor grey that
   does not flip with mode at all — it measured 2.06:1 dark and 1.95:1 light
   against a 4.5 floor, so this file carried a `#rightSidebar .form__label`
   patch that corrected the colour and left the size alone. `.sidebar-field-label`
   was a drawer-only third spelling at 0.9em sentence case. Between them,
   right_sidebar.js opened the same kind of form three different ways: the
   category form and the loan-event wizard on .prism-field-label, add-cash on
   .sidebar-field-label, transaction detail on the vendor class.

   All 38 sites now use .prism-field-label — the app-wide replacement the
   FORM FIELD VOCABULARY block above already documented, and the one the two
   asset forms use — so both rules here are dead and gone. right_sidebar.js was
   the only drawer surface holding .form__label, which is what made the patch
   removable rather than merely unused.

   Note the labels are now UPPERCASE where add-cash and upload-document had
   them in sentence case, and 12px where transaction detail had 11px. That is
   the consolidation, not a regression. */

/* Modifier on .sidebar-footer-actions, which already supplies the flex row,
   gap, padding-top and tokenised border-top. */
.sidebar-footer-actions--end {
    justify-content: flex-end;
}

/* Colour swatch in the category/tag pickers. Selection used to be three
   imperative writes per swatch (borderColor '#fff', a white glow, transform);
   it is one classList.toggle now, which is also why the white literals went. */
.sidebar-swatch {
    border: 2px solid transparent;
    transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.sidebar-swatch.is-selected {
    border-color: var(--prism-text);
    box-shadow: 0 0 10px var(--prism-text-dim);
    transform: scale(1.15);
}

/* Icon tile in the emoji pickers. Same story: eight reset/select pairs across
   the add- and edit- renderers all wrote the same two literals. */
.sidebar-icon-opt {
    background: var(--prism-surface-hover);
    border: 2px solid transparent;
    border-radius: 8px;
    transition: background 0.2s ease, border-color 0.2s ease;
}
.sidebar-icon-opt:hover {
    background: var(--prism-tint-3);
}
.sidebar-icon-opt.is-selected {
    background: var(--prism-tint-4);
    border-color: var(--prism-accent);
}

/* Segmented control (income/expense, inherit/custom). The active state was a
   three-line assignment triad hardcoding rgba(29,140,248,…) — the literal
   blue, so every non-blue skin rendered a blue pill. */
.sidebar-seg-btn {
    background: transparent;
    border: 1px solid var(--prism-border-strong);
    color: var(--prism-text-muted);
    transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.sidebar-seg-btn.is-active {
    background: var(--prism-tint-4);
    border-color: var(--prism-accent-border);
    color: var(--prism-accent-text);
}

/* Success-tinted preview tile. Used by the loan-event wizard's #lew-preview and
   by #lew-gap, the closing-costs wire check beside it — the category form's copy
   became the pinned identity chip below, which is a different object: always
   visible rather than revealed, and carrying the category's own colour rather
   than a success tint. The `.sidebar-preview-row` child went with it and had no
   other consumer.

   IT SETS ITS OWN FOREGROUND, and that is not decoration. The tile paints a
   saturated wash and for a long time declared only a background, so its text
   took whatever the drawer had — a neutral grey ramp chosen against neutral
   grounds, landing here on a dark teal one nobody had measured it against.
   Measured now: --prism-text-muted was 4.89:1 dark and 5.96:1 light. The dark
   figure clears AA by 0.4, which is the number a desaturated blue-grey on a
   saturated teal earns; it reads worse than it measures, which is why this was
   reported in dark and not in light.

   The button inside it was a straight failure rather than a near miss —
   --prism-text-dim measured 4.24:1 dark and 4.31:1 light, under AA in BOTH
   modes at 10px, and small enough that nobody had noticed. Hence a rule for it
   too: a component that paints its own ground owns the foreground of whatever
   sits on it. Now 7.96/12.12 dark and 6.89/12.68 light.
   test_the_drawer_preview_tile_owns_a_readable_foreground holds both, and its
   first assertion is the one that matters — that a `color` is declared at all,
   since inheriting is how this got here. */
.sidebar-preview-tile {
    background: var(--prism-success-wash);
    border: 1px solid var(--prism-success);
    border-radius: 10px;
    color: var(--prism-text-2);
}

.sidebar-preview-tile .lew-edit-btn {
    color: var(--prism-text);
    border-color: var(--prism-success);
}

.sidebar-preview-tile .lew-edit-btn:hover {
    color: var(--prism-text);
    border-color: var(--prism-text);
}

/* ============================================================================
   CATEGORY FORM (right drawer)
   Used in: right_sidebar.js — openCategoryForm, which is the single renderer
   behind "New Parent Category", "Add Subcategories" and "Edit Category".

   Those were three renderers, ~1400 lines, sharing nothing but
   .sidebar-form-shell above. Each hand-wrote its own swatch row, emoji grid,
   custom-emoji input, preview tile and footer, so the three forms had already
   drifted on label style, swatch size and whether the colour picker started
   open. This section is what one form needs that the shared vocabulary above
   does not already give it — everything else here is .prism-field,
   .sidebar-swatch, .sidebar-icon-opt, .sidebar-seg-btn and .prism-btn-*.

   THE SHAPE. A category has four properties, and the old form spent roughly
   1400px of scroll on them in a 400px-wide drawer: an always-open 200px HSV
   gradient below a swatch row that already answered the question, an always-open
   200px emoji grid, and — below both, so never on screen while you typed — the
   live preview. Colour and icon are now one row of two buttons that open
   popovers on demand, and the preview is a chip pinned above the fields. The
   form fits without scrolling, which is the whole point of the change.

   COLOUR THAT IS DATA, NOT THEME. A category's colour is persisted and re-parsed
   by hex regexes, so it can never be a token — but
   test_drawer_js_has_no_hardcoded_theming_colours holds right_sidebar.js at ZERO
   inline theming literals. The chip and the dots resolve that by reading
   --cf-color / --cf-tint, which the JS sets with setProperty(). A custom
   property is not a themed property, so the ratchet's regex cannot match it, and
   every declaration below still names a token as its fallback. Do not "simplify"
   these into inline background/color writes.
   ============================================================================ */

/* The pinned identity chip. Sits OUTSIDE the scroll region as a flex-shrink: 0
   child of .sidebar-form-shell, so it stays put while the fields move. It is
   the preview, and it is at the top because the old one was at the bottom. */
.cat-form-identity {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-shrink: 0;
    margin-bottom: 22px;
    padding: 14px 16px;
    background: var(--prism-veil-2);
    border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-panel);
}

/* 48px because it is the same glyph the ledger draws at .led-icon, and the two
   should read as the same object seen at two sizes. */
.cat-form-identity-glyph {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    font-size: 1.6rem;
    line-height: 1;
    border-radius: 14px;
    background: var(--cf-tint, var(--prism-tint-3));
    color: var(--cf-color, var(--prism-accent));
    box-shadow: inset 0 0 0 1px var(--cf-color, var(--prism-border));
    transition: background 0.2s ease, box-shadow 0.2s ease;
}

.cat-form-identity-glyph img {
    width: 1em;
    height: 1em;
    object-fit: contain;
}

.cat-form-identity-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

/* Wraps rather than ellipsing: a category name is the thing being edited, and
   truncating it hides the character someone just mistyped. Two lines maximum,
   which is what the 48px glyph beside it affords. */
.cat-form-identity-name {
    font-size: var(--prism-fs-lg);
    font-weight: 700;
    line-height: 1.25;
    color: var(--prism-text);
    overflow-wrap: anywhere;
}

.cat-form-identity-meta {
    font-size: var(--prism-fs-xs);
    font-weight: 600;
    letter-spacing: 0.3px;
    text-transform: uppercase;
    color: var(--prism-text-muted);
}

/* ---- Appearance row: two triggers, two popovers ---- */

.cat-form-appearance {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}

.cat-form-appearance-btn {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 10px 12px;
    font-family: inherit;
    font-size: var(--prism-fs-sm);
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    color: var(--prism-text-2);
    background: var(--prism-surface-2);
    border: 1px solid var(--prism-border-strong);
    border-radius: 10px;
    transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.cat-form-appearance-btn:hover {
    background: var(--prism-surface-hover);
    border-color: var(--prism-accent-border);
    color: var(--prism-text);
}

.cat-form-appearance-btn:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

/* The open trigger keeps the accent while its popover is showing, so it is
   obvious which of the two the panel below belongs to. Driven by the ARIA
   state rather than a class: aria-expanded has to be maintained for screen
   readers anyway, and a second source of truth would be one more thing to
   forget to toggle. */
.cat-form-appearance-btn[aria-expanded="true"] {
    border-color: var(--prism-accent-text);
    background: var(--prism-tint-4);
    color: var(--prism-text);
}

.cat-form-appearance-dot {
    flex: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--cf-color, var(--prism-accent));
    box-shadow: inset 0 0 0 1px var(--prism-border-strong);
}

.cat-form-appearance-glyph {
    flex: none;
    width: 20px;
    font-size: 1.05rem;
    line-height: 1;
    text-align: center;
}

.cat-form-appearance-glyph img {
    width: 1em;
    height: 1em;
    object-fit: contain;
    vertical-align: middle;
}

/* Zero-height positioning context. The popovers are absolutely positioned
   against this so they overlay the fields below rather than displacing them —
   a panel that pushes the Save button off-screen is the problem this whole
   redesign is fixing.

   THE ANCHOR'S WIDTH IS THE PANEL'S WIDTH, because the panel is `left: 0;
   right: 0` against it. That makes the anchor's own layout load-bearing in a
   way a zero-height element normally is not, and it is the thing to check first
   if a panel ever renders the wrong size: see the grid override further down
   for what happened when the anchor became a grid item. Any new placement must
   put it somewhere it spans the full width it wants the panel to have. */
.cat-form-appearance-anchor {
    position: relative;
    height: 0;
}

/* OPAQUE, and named -popover so the glass rule exempts it: this floats over
   arbitrary form content and has to stay readable. Same treatment as
   .tx-cat-menu, but the shadow reads --prism-shadow-soft rather than restating
   a literal pair, which is what keeps it out of the light-mode override count. */
.cat-form-popover {
    position: absolute;
    top: 8px;
    left: 0;
    right: 0;
    z-index: 1060;
    /* Stated rather than inherited. `left: 0; right: 0` plus padding makes this
       28px wider than its container under content-box, and the drawer's scroll
       region cannot be overflow-y:auto with overflow-x:visible — the other axis
       computes to auto too — so the panel gained a horizontal scrollbar and
       focusing the search field scrolled the whole form sideways, clipping
       every field label. The app only avoids that because Bootstrap happens to
       set a global border-box, which is precisely the vendor coupling the
       decoupling section at the bottom of this file exists to undo. */
    box-sizing: border-box;
    padding: 14px;
    background: var(--prism-panel);
    border: 1px solid var(--prism-accent-border);
    border-radius: var(--prism-radius-panel);
    box-shadow: 0 18px 44px var(--prism-shadow-soft);
    animation: catFormPopIn 0.16s ease both;
}

.cat-form-popover[hidden] {
    display: none;
}

/* Flipped by JS when the trigger is close enough to the bottom of the drawer
   that a downward panel would be clipped — .sidebar-content is overflow-y:auto,
   so it clips rather than growing. */
.cat-form-popover.is-above {
    top: auto;
    bottom: 8px;
}

@keyframes catFormPopIn {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}

.cat-form-popover-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
    justify-items: center;
}

/* Swapped out when the custom picker is showing. See the note on
   .cat-form-icon-opt[hidden] for why the explicit rule is required. */
.cat-form-popover-grid[hidden] {
    display: none;
}

/* A preset colour. The fill is --cf-color rather than an inline background,
   for the reason given in this section's header: the JS may not write a colour
   into a themed property. .sidebar-swatch above supplies the selection ring. */
.cat-form-swatch {
    width: 30px;
    height: 30px;
    padding: 0;
    cursor: pointer;
    border-radius: 50%;
    background: var(--cf-color, var(--prism-accent));
}

.cat-form-swatch:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

.cat-form-popover-foot {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--prism-border);
}

/* The emoji grid. auto-fill rather than a fixed count so it reflows with the
   drawer, which is clamp(400px, var(--rsb-pref, 30vw), 900px) wide. */
.cat-form-icon-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
    gap: 6px;
    max-height: 172px;
    margin-top: 10px;
    padding-right: 4px;
}

/* Square tiles. .sidebar-icon-opt above supplies the fill, hover and selected
   states; this is the geometry a <button> needs that a <div> did not. */
.cat-form-icon-opt {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1;
    padding: 0;
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
}

/* The search filters by toggling .hidden on these, and `display: flex` above
   outranks the UA stylesheet's `[hidden] { display: none }` — without this the
   filter sets the property and nothing moves on screen. Same trap as
   .cat-form-icon-empty and .cat-form-popover-grid below. */
.cat-form-icon-opt[hidden] {
    display: none;
}

.cat-form-icon-opt:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 1px;
}

.cat-form-icon-opt img {
    width: 1em;
    height: 1em;
    object-fit: contain;
}

.cat-form-icon-empty {
    display: block;
    grid-column: 1 / -1;
    padding: 18px 4px;
    text-align: center;
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
}

/* `display: block` above outranks the UA stylesheet's `[hidden] { display: none }`,
   so without this the "nothing matches" line shows permanently — including
   underneath a grid full of results. Any author display rule on an element the
   JS toggles with .hidden needs its own [hidden] companion. */
.cat-form-icon-empty[hidden] {
    display: none;
}

/* ---- Batch row: one subcategory per line ---- */

/* Was a ~200px-tall card per row, each carrying its own copy of the swatch row
   and the full 113-emoji grid — five rows meant 565 emoji nodes. The popovers
   are singletons now, moved into the active row's anchor, so a row is a line. */
.cat-form-row {
    display: grid;
    grid-template-columns: 34px 22px minmax(0, 1fr) 28px;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

/* The popover anchor is a FIFTH child of a four-column grid, so it wraps onto
   an implicit second row and lands in column one — 34px wide. The popover is
   `left: 0; right: 0` against it, so both panels rendered 34px across with
   their contents spilling out: a one-character-wide search box and no visible
   grid at all. Spanning every column is what makes the anchor mean "the width
   of this row"; `gap` would otherwise also push it down, hence the negative
   margin against the row's own 8px. */
.cat-form-row > .cat-form-appearance-anchor {
    grid-column: 1 / -1;
    margin-top: -8px;
}

.cat-form-row-glyph,
.cat-form-row-dot {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 34px;
    padding: 0;
    cursor: pointer;
    background: var(--prism-surface-2);
    border: 1px solid var(--prism-border-strong);
    border-radius: 9px;
    transition: border-color 0.2s ease, background 0.2s ease;
}

.cat-form-row-glyph {
    width: 34px;
    font-size: 1.05rem;
    line-height: 1;
}

.cat-form-row-dot {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--cf-color, var(--prism-accent));
    box-shadow: inset 0 0 0 1px var(--prism-border-strong);
}

.cat-form-row-glyph:hover,
.cat-form-row-dot:hover {
    border-color: var(--prism-accent-text);
}

.cat-form-row-glyph:focus-visible,
.cat-form-row-dot:focus-visible,
.cat-form-row-remove:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

.cat-form-row-remove {
    width: 28px;
    height: 28px;
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    color: var(--prism-text-muted);
    background: transparent;
    border: 0;
    border-radius: 7px;
    transition: color 0.2s ease, background 0.2s ease;
}

.cat-form-row-remove:hover {
    color: var(--prism-danger);
    background: var(--prism-danger-wash);
}

/* ---- Conditional retag notice ---- */

/* Only rendered once the name or parent actually changes and the impact check
   reports live transactions. It used to sit second from the top of the form,
   permanently, with a three-line paragraph explaining a situation most edits
   never enter — so it was noise on every edit and easy to stop reading before
   the one edit where it mattered. */
.cat-form-retag {
    margin-top: 4px;
    padding: 13px 15px;
    background: var(--prism-warning-wash);
    border: 1px solid var(--prism-warning-border);
    border-radius: var(--prism-radius-panel);
    animation: catFormPopIn 0.2s ease both;
}

.cat-form-retag[hidden] {
    display: none;
}

.cat-form-retag-head {
    margin-bottom: 10px;
    font-size: var(--prism-fs-sm);
    line-height: 1.45;
    color: var(--prism-text-2);
}

/* tabular-nums so the figure does not jitter as the debounced impact check
   replaces it while the user is still typing. */
.cat-form-retag-count {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--prism-warning);
}

/* ---- The HSV picker, as it appears inside the colour popover ---- */

/* Layout only. .modern-color-picker itself is untouched, because tags.html
   mounts the same component through the same public API and a change there
   would land on a page this work never looked at. */
.cat-form-color-popover .modern-color-picker {
    margin-top: 0;
    padding: 0;
    background: none;
    border: 0;
}

.cat-form-color-popover .color-picker-main {
    height: 124px;
}

/* A category colour is never copied to a clipboard and is never typed as HSL;
   both controls are pure width in a panel this narrow. */
.cat-form-color-popover .color-picker-copy-btn,
.cat-form-color-popover .color-picker-format-select {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .cat-form-popover,
    .cat-form-retag {
        animation: none;
    }
}

/* ===== LOAN EVENT WIZARD (right-sidebar) =====
   Reuses prism-text-muted, prism-text-danger, prism-card-subtle, prism-btn-danger,
   prism-btn-sm from above. Only defines what has no existing equivalent.

   Three of its classes used to have no definition anywhere and rendered as
   browser defaults inside an otherwise styled drawer: `.lew-error` (the
   validation banner, unstyled body text), `.lew-btn-delete` (grey system
   button), and `.lew-field-hint`, which was defined only in admin.css -- whose
   own comment recorded that the wizard needed it -- so it was styled on the four
   admin pages and unstyled on the two asset pages where the wizard actually
   runs. All three are gone from the JS now: the banner is `.sidebar-form-error`,
   the button is `prism-btn-danger prism-btn-sm`, and the hint is
   `.prism-field-hint`, alongside `.prism-field-label` for the label and
   `.prism-choice-card` for the type picker. What is left below is what is
   genuinely specific to a form rendered inside the drawer. */

/* "+ Add" button in the Loan Events section header */
.lew-add-btn {
    background: none;
    border: 1px solid var(--prism-accent2);
    color: var(--prism-accent2-text);
    border-radius: 4px;
    font-size: var(--prism-fs-2xs);
    padding: 2px 8px;
    cursor: pointer;
    line-height: 1.6;
    transition: background 0.15s;
}
.lew-add-btn:hover { background: rgba(var(--prism-accent2-rgb), 0.12); }

/* "Edit" button on existing loan event cards */
.lew-edit-btn {
    background: none;border: 1px solid var(--prism-border-strong);
    color: var(--prism-text-dim);
    border-radius: 4px;
    font-size: var(--prism-fs-2xs);
    padding: 1px 6px;
    cursor: pointer;
    line-height: 1.6;
    transition: border-color 0.15s, color 0.15s;
}
.lew-edit-btn:hover {border-color: var(--prism-border-strong); color: var(--prism-text-2); }

/* "Not sure?" <details> — structural only; color inherits from prism-text-muted on the element */
.lew-not-sure summary { cursor: pointer; font-weight: 500; list-style: none; padding: 6px 0; }
.lew-not-sure summary::-webkit-details-marker { display: none; }
.lew-decision-guide {
    padding: 10px 12px;background: var(--prism-surface-2);
    border-radius: 6px;
    line-height: 1.9;
    margin-top: 6px;
}

/* Step 2 type banner — background needs dark/light pair; color reuses prism-text-muted */
.lew-type-header {
    font-size: var(--prism-fs-2xs); font-weight: 600;
    margin-bottom: 14px; padding: 6px 10px;background: var(--prism-surface-2); border-radius: 6px;
}

/* Two-dot step rail. The page wizard's rail is built for four labelled steps
   across a full-width panel and does not shrink to a 400px drawer, but the
   drawer still has to say which of two steps it is on. */
.lew-steps {
    display: flex;
    align-items: center;
    gap: 7px;
    margin-bottom: 14px;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--prism-text-muted);
}
.lew-steps-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--prism-border-strong);
}
.lew-steps-dot.is-active { background: var(--prism-accent-text); }

/* Sidebar inputs — no existing equivalent handles the dark sidebar context */
.lew-input {
    width: 100%;
    background: var(--prism-surface-raised);border: 1px solid var(--prism-border-strong);
    border-radius: 6px;
    padding: 6px 10px;
    font-size: var(--prism-fs-sm);color: var(--prism-text);
    transition: border-color 0.15s;
    box-sizing: border-box;
}
.lew-input:focus { outline: none;border-color: var(--prism-accent-border); }
.lew-input::placeholder {color: var(--prism-text-faint); }

/* End-date ✕ clear button — color inherits from prism-text-muted on the element */
.lew-clear-btn { background: none; border: none; cursor: pointer; font-size: 1.125rem; line-height: 1; padding: 0 2px; }

/* Back button */
.lew-btn-back {
    background: none;border: 1px solid var(--prism-border-strong);color: var(--prism-text-muted);
    border-radius: 6px;
    padding: 5px 14px;
    cursor: pointer;
    font-size: var(--prism-fs-sm);
    flex-shrink: 0;
    transition: border-color 0.15s;
}
.lew-btn-back:hover {border-color: var(--prism-border-strong); }

/* Save button — solid accent fill, stretches to fill remaining row space.
   --prism-on-accent, not #fff: the fill follows the skin, and white on the
   bright ones measures 1.9:1 (teal), 2.2:1 (amber) and 2.4:1 (green). It does
   not flip with mode, because what it sits on does not either. */
.lew-btn-save {
    flex: 1;
    background: var(--prism-accent2);
    border: none;
    color: var(--prism-on-accent);
    border-radius: 6px;
    padding: 6px 14px;
    cursor: pointer;
    font-size: var(--prism-fs-sm);
    font-weight: 600;
    transition: opacity 0.15s;
}
.lew-btn-save:hover { opacity: 0.88; }
.lew-btn-save:disabled { opacity: 0.55; cursor: not-allowed; }

/* ===== FILE DROP ZONE =====
   The click-or-drag file target every drawer form uses. It lived in
   asset-transactions.html's <style> as hover/selected rules only, with the base look
   (dashed border, padding, surface) written inline in right_sidebar.js's
   upload-document markup -- so a second page that wanted the same control got
   a bare `Choose File` button, which is exactly what the admin Google Drive
   form shipped with first. The base is here now, so the markup is a class and
   nothing else.

   Neutral at rest, theme accent on hover and once a file is chosen: the same
   rule the table surfaces follow. The markup must not restate any of this
   inline: right_sidebar.js used to, and an inline `border` shorthand outranks
   the :hover rule below, so the zone stopped reacting to hover and to a chosen
   file. That is why the asset-page rules this replaced all carried !important --
   they existed to beat those inline styles. The inline styles are gone, so
   these do not need it. */
.file-upload-wrapper {
    border: 2px dashed var(--prism-border-strong);
    border-radius: 10px;
    padding: 22px;
    text-align: center;
    background: var(--prism-surface-2);
    cursor: pointer;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.file-upload-wrapper:hover,
.file-upload-wrapper.file-selected {
    border-color: var(--prism-accent-border);
    background-color: var(--prism-surface-hover);
}

.file-upload-wrapper .tim-icons {
    font-size: 2em;
    margin-bottom: 10px;
    transition: transform 0.2s ease;
}

.file-upload-wrapper:hover .tim-icons {
    transform: scale(1.05);
}

.file-upload-wrapper .upload-text {
    font-weight: 600;
    margin-bottom: 5px;
}

/* One chosen file, listed back. --prism-text rather than #fff: the name is
   read against both grounds, and the old literal was invisible in light mode. */
.selected-file-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 12px;
    margin: 5px 0;
    background-color: var(--prism-surface-2);
    border-radius: 6px;
    border-left: 3px solid var(--prism-accent-border);
}

/* display:flex above outranks the UA's [hidden] rule, so the attribute alone
   leaves an empty pill sitting in the drop zone. Every component in this file
   that sets `display` and is toggled by `hidden` carries one of these. */
.selected-file-item[hidden] {
    display: none;
}

.selected-file-name {
    flex-grow: 1;
    color: var(--prism-text);
    font-size: 0.9em;
    word-break: break-all;
    text-align: left;
}

.selected-file-size {
    color: var(--prism-text-muted);
    font-size: 0.8em;
    margin-left: 10px;
    white-space: nowrap;
}

/* ===== CATEGORY TRANSACTION SIDEBAR ENHANCEMENTS ===== */
.category-transactions-shell {
    position: relative;
}

.category-transactions-scroll {
    position: relative;
    /* Guaranteed floor so this region (and the sticky "Hide metrics" toggle
       inside it) never collapses to ~0 when a tall Key-metrics table + chart
       overflow a short viewport. Pairs with .category-metrics-collapsible
       yielding/scrolling internally below. */
    min-height: 96px;
}

.category-sticky-bar {
    position: sticky;
    top: 0;
    z-index: 6;
    position: -webkit-sticky;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    /* min-height keeps the centered month pill's backdrop intact now that the
       metrics toggle no longer lives in (and sizes) this bar. */
    min-height: 34px;
    padding: 4px 0 6px;
    margin: 0 -20px 6px -20px;
    /* Fades up to the drawer's own ground, so the bar actually occludes what
       scrolls under it. A 6% veil was enough to fake that on the old opaque
       panel; against a textured ground it is not, and the rows read straight
       through. Second consumer of --prism-drawer-ground, which is also what
       keeps that token honest — test_no_unused_prism_tokens is bidirectional. */
    background: linear-gradient(180deg, transparent 0%, var(--prism-drawer-ground) 100%);
    backdrop-filter: blur(4px);
    border-bottom: 1px solid var(--prism-border-faint);
}

.category-metrics-collapsible {
    /* Cap relative to the viewport so a long years table can't push the
       transactions list + "Hide metrics" toggle off-screen on short screens.
       When the table exceeds the cap it scrolls *within* this panel. The
       max-height also serves as the concrete endpoint the collapse animates
       to/from (40vh ↔ 0). */
    max-height: min(1200px, 40vh);
    overflow-y: auto;
    overflow-x: hidden;
    /* Keep scroll momentum inside this panel — don't chain to the sidebar /
       page when the years table reaches its top or bottom. Matches the
       transactions scroll region below. */
    overscroll-behavior: contain;
    /* Size the .category-metrics-region wrapper to this panel's CONTENT (so the
       region grows to the full table, capped at 40vh, when there's room) while
       still being able to shrink + scroll internally on short viewports. Uses
       flex-grow:0 (basis:auto = content) — NOT flex:1, which collapsed the
       wrapper's content size to ~0 and pinned it at the floor even with plenty
       of room. min-height:0 lets it shrink below content so the internal
       scrollbar engages instead of overflowing. */
    flex: 0 1 auto;
    min-height: 0;
    transition: max-height 0.35s ease, opacity 0.35s ease, padding 0.35s ease;
}

.category-metrics-collapsible.collapsed {
    max-height: 0 !important;
    opacity: 0;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
    border-bottom: none !important;
}

.category-metrics-toggle-wrapper {
    padding: 6px 20px 0 20px;
    display: flex;
    justify-content: flex-end;
}

/* Positioning context for the metrics-toggle overlay AND the flex item that
   sizes the Key-metrics panel. Wraps the collapsible panel + the toggle button
   so the button can be absolutely positioned over the panel's top-right corner.
   flex:0 1 auto makes the region shrinkable, so on a short viewport it yields
   space to the transactions list (which keeps its 96px min + internal scroll)
   instead of pushing it out of the clipped sidebar content. The collapsible
   child fills this region and scrolls internally. */
.category-metrics-region {
    position: relative;
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    min-height: 0;
}

/* While expanded, cap the region at 40vh and hold a usable floor so that WHEN
   it must shrink on a short viewport it still shows the header + a few rows
   (and scrolls internally) rather than collapsing to just the header. When
   there's room the region sizes to its content (the flex:0 1 auto inner panel),
   so this floor isn't binding. Both constraints drop when collapsed (selector
   no longer matches) so the region can close fully and free its space. */
.category-metrics-region:has(> .category-metrics-collapsible:not(.collapsed)) {
    min-height: 220px;
    max-height: min(1200px, 40vh);
}

.category-metrics-toggle {
    /* Overlay in the top-right corner of the Key-metrics container (the region
       wrapper is position:relative). Sits above the panel content and stays put
       while the table scrolls internally; when the panel is collapsed it floats
       at the region's top edge so it's always reachable to re-expand. */
    position: absolute;
    top: 14px;
    right: 16px;
    z-index: 10;
    /* Accent-keyed in both modes. It used to be neutral white in dark and
       accent in light, which is the whole reason it needed an override —
       the two modes were describing the same chip two different ways.
       .sidebar-close already wears --prism-accent-text on this surface. */
    background: var(--prism-tint-2);
    border: 1px solid var(--prism-accent-border);
    color: var(--prism-accent-text);
    border-radius: 999px;
    padding: 6px 12px;
    line-height: 1.2;
    box-shadow: 0 10px 24px var(--prism-shadow-soft);
}

.category-metrics-toggle:hover {
    background: rgba(var(--prism-accent-rgb), 0.15);
}

.category-sticky-month {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 1;
    text-align: center;
    padding: 4px 14px;
    margin: 0;
    font-weight: 800;
    font-size: var(--prism-fs-xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    -webkit-font-smoothing: antialiased;
    pointer-events: none;color: var(--prism-text);
    /* Near-opaque on purpose: this pill floats over the scrolling rows, so it
       is a floating layer in the sense SURFACE_EXCLUDE means, not part of the
       drawer's own material. --prism-overlay is that tier in both modes. */
    background: var(--prism-overlay);
    border: 1px solid var(--prism-accent-border);
    border-radius: 999px;
    box-shadow: 0 4px 12px var(--prism-shadow-soft);
}

.category-month-header {
    text-align: center;
    margin: 0 0 10px 0;
    padding: 8px 0 6px;
    font-weight: 700;
    position: relative;
    color: var(--prism-text);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.category-month-header::after {
    content: '';
    position: absolute;
    left: 20%;
    right: 20%;
    bottom: 0;
    height: 1px;
    background: var(--prism-border-faint);
}

/* ===== RESPONSIVE DESIGN ===== */

/* Tablet and smaller */
@media (max-width: 768px) {
    /* Layout only. The drawer's surface is opaque and blur-free at every
       width, so phone width gets the same treatment as desktop instead of
       opting out of it — which is what previously made the redesign invisible
       on a phone.

       The drawer goes full-width by redefining the WIDTH TOKEN rather than by
       restating `width: 100%; right: -100%` on .right-sidebar. Everything that
       positions itself against the drawer then follows automatically, which is
       the whole point: .prism-scroll-progress insets by var(--rsb-w) so it
       stops where the drawer starts, and when this block hardcoded the width
       instead, that inset kept the desktop 400px minimum here. On a 600px
       phone the drawer covered the screen and a 200px stub of the rail stayed
       painted over its header, because the bar outranks the drawer (1055 vs
       1051). Anything else that ever aligns to the drawer edge would have
       inherited the same bug.

       `calc(-1 * var(--rsb-w))` on .right-sidebar resolves to -100% here, and
       percentages on a fixed element resolve against the viewport, so the
       off-screen rest position is unchanged. The one consumer that must NOT
       follow the token is the content/footer/navbar padding — `100% + 30px`
       of padding would be absurd — and it is already overridden below to a
       flat 30px, because phone width overlays the drawer instead of splitting
       the layout with it. */
    :root {
        --rsb-w: 100%;
    }

    /* Nothing to resize once the drawer IS the screen, and an 8px strip down
       the left edge would fight the drawer's own scrolling on touch. The JS
       leaves --rsb-pref alone here either way: the token above redefines
       --rsb-w outright, so a stored preference is not read at this width. */
    .right-sidebar-resize {
        display: none;
    }

    .sidebar-header {
        /* Horizontal only, like the base rule -- height stays pinned to
           --prism-navbar-h (~64px) and vertical centering comes from
           align-items:center, not from padding. Vertical padding here on top
           of that fixed, border-box height left no room for the 40px close
           button: 20px top + 20px bottom + the 3px border alone is 43px,
           more than half the box, and the button overflowed across the
           border. */
        padding: 0 20px;
    }
    
    .sidebar-content {
        padding: 20px;
    }

    /* On mobile, fall back to overlay behavior — there isn't room to split. */
    body.right-sidebar-open .main-panel > .content,
    body.right-sidebar-open .main-panel > footer,
    body.right-sidebar-open .main-panel > .navbar.navbar-absolute {
        padding-right: 30px;
    }
}

/* Small mobile */
@media (max-width: 480px) {
    .sidebar-stats-summary {
        grid-template-columns: 1fr;
    }
}

/* ===== MODERN TOGGLE SWITCH ===== */
.prism-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    user-select: none;
}

.prism-toggle-input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.prism-toggle-slider {
    position: relative;
    display: inline-block;
    width: 50px;
    height: 26px;background: var(--prism-surface-hover);border: 2px solid var(--prism-accent-border);
    border-radius: 26px;
    transition: all 0.3s ease;
}

.prism-toggle-slider::before {
    content: '';
    position: absolute;
    width: 18px;
    height: 18px;
    left: 3px;
    top: 2px;
    background: #adb5bd;
    border-radius: 50%;
    transition: all 0.3s ease;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.prism-toggle-input:checked + .prism-toggle-slider {
    /* Was accent -> #5e72e4, a hardcoded indigo that stayed indigo on all ten
       skins. On amber or green the toggle was the one control on screen still
       insisting the app was blue. */
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent3) 100%);
    border-color: var(--prism-accent);
}

.prism-toggle-input:checked + .prism-toggle-slider::before {
    transform: translateX(24px);
    background: #fff;
    box-shadow: 0 2px 6px rgba(var(--prism-accent-rgb), 0.4);
}

.prism-toggle-input:focus + .prism-toggle-slider {
    box-shadow: 0 0 0 3px rgba(var(--prism-accent-rgb), 0.2);
}

.prism-toggle-label {
    font-size: 0.95em;
    font-weight: 500;color: var(--prism-text-muted);
    transition: color 0.3s ease;
    margin: 0;
}

.prism-toggle-input:checked ~ .prism-toggle-label {
    color: var(--prism-accent-text);
}

.prism-toggle:hover .prism-toggle-slider {
    border-color: rgba(var(--prism-accent-rgb), 0.5);
}

.prism-toggle:hover .prism-toggle-label {color: var(--prism-text);
}

.prism-toggle-input:checked ~ .prism-toggle-label {
    color: var(--prism-accent-text);
}

/* White content mode */

.white-content .prism-toggle-slider::before {
    background: #525f7f;
}

/* ===== REPORT TABLE STYLES ===== */
/* Modern financial report table styling with category hierarchy */
/* Neutral border, not --prism-accent-border: the container is the table's
   outermost piece of furniture, and outlining every table in the accent is the
   same "accent as dye" the veil ladder removed from cards. */
.report-table-container {background: var(--prism-surface-2);
    border-radius: 12px;
    overflow: hidden;border: 1px solid var(--prism-border);
}

.report-table {
    width: 100%;
    margin: 0;
    border-collapse: separate;
    border-spacing: 0;
    font-family: var(--prism-font-body);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* ===== TABLE SURFACES: NEUTRAL AT REST, ACCENT ON STATE =====
   Used by reports.html and real_estate_schedule.html via .report-table.

   The header was rgba(accent, .15)->.08 with an accent bottom border, and the
   row hover was rgba(accent, .05) — three times fainter than the header. So the
   heaviest accent on the page was the one element that never changes, and the
   element that responds to the pointer was the quietest thing on it. That is
   the budget backwards: a table is content, and the accent's job is to mark
   which row you are on, not to decorate the furniture above it.

   It also multiplies. Ten skins times two modes is twenty grounds, and an
   accent-tinted header means every dense numeric table has to clear AA against
   all twenty. Neutral furniture reduces that to the handful of state surfaces
   that genuinely earn a colour.

   OPAQUE, not a veil, and that is the one place tables differ from cards: this
   header is position:sticky, so rows scroll underneath it. A translucent header
   shows the data sliding through the column labels. CLAUDE.md's rule against
   opaque content surfaces exempts chrome that floats over arbitrary content —
   dropdowns, modals — and a pinned header is exactly that. */
.report-table thead th {
    background: var(--prism-surface-raised);
    color: var(--prism-text);
    padding: 16px 12px;
    font-family: var(--prism-font-display);
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.85rem;
    letter-spacing: 0.08em;
    border-bottom: 2px solid var(--prism-border-strong);
    position: sticky;
    top: 0;
    z-index: 10;
}

.report-table tbody td {
    padding: 12px;border-bottom: 1px solid var(--prism-border-faint);color: var(--prism-text-2);
}

/* Section headers (INCOME, OPERATING EXPENSES, etc) */
.report-section-header {
    padding: 24px 12px !important;
    font-family: var(--prism-font-display);
    font-weight: 700;
    text-transform: uppercase;
    font-size: 1.1rem;
    letter-spacing: 0.12em;color: var(--prism-accent-text) !important;
    background: linear-gradient(90deg, rgba(var(--prism-accent-rgb), 0.08) 0%, transparent 100%) !important;
    border-left: 4px solid var(--prism-accent);
}

/* Tabular figures across the whole report table so numeric columns align */
.report-table tbody td {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Individual category rows. No `color` here: `.report-table tbody td` already
   sets var(--prism-text-2), and the #e0e0e0 this used to carry overrode that
   token with a light grey that has no light-mode value — the ramp exists so
   this rule does not have to have an opinion. */
.report-category-row td {
    padding-left: 36px !important;
    font-size: 0.95rem;
}

/* Hover is where the accent belongs, and it steps UP to tint-2 (.08 dark /
   .06 light) from the old flat .05 — it has to out-read the header, not lose
   to it. */
.report-category-row:hover {
    background: var(--prism-tint-2);
}

.report-category-row:hover td {
    color: var(--prism-text);
}

/* Subtotal rows (Total Rental Income, Total Operating Expenses, etc) */
.report-subtotal-row {
    background: linear-gradient(90deg, rgba(127, 146, 193, 0.25) 0%, rgba(127, 146, 193, 0.15) 100%) !important;
    border-top: 1px solid rgba(127, 146, 193, 0.4) !important;
    border-bottom: 1px solid rgba(127, 146, 193, 0.4) !important;
}

.report-subtotal-row td {
    padding: 11px 12px !important;
    font-weight: 600;
    font-size: 0.95rem;
    color: #e8edfb;
    font-variant-numeric: tabular-nums;
}

/* Grand total rows (All Income, All Operating Expenses) */
.report-grandtotal-row {
    background: linear-gradient(90deg, rgba(219, 152, 85, 0.35) 0%, rgba(219, 152, 85, 0.25) 100%) !important;
    border-top: 2px solid rgba(219, 152, 85, 0.6) !important;
    border-bottom: 2px solid rgba(219, 152, 85, 0.6) !important;
}

.report-grandtotal-row td {
    padding: 14px 12px !important;
    font-weight: 700;
    font-size: 1rem;
    color: #ffd89b;
    font-variant-numeric: tabular-nums;
}

.report-grandtotal-row td:first-child {
    text-transform: uppercase;
    letter-spacing: 0.8px;
}

/* Net Operating Income row */
.report-noi-row {
    background: linear-gradient(90deg, rgba(39, 151, 213, 0.4) 0%, rgba(39, 151, 213, 0.3) 100%) !important;
    border-top: 3px solid rgba(39, 151, 213, 0.8) !important;
    border-bottom: 3px solid rgba(39, 151, 213, 0.8) !important;
}

.report-noi-row td {
    padding: 16px 12px !important;
    font-weight: 700;
    font-size: 1.05rem;color: var(--prism-accent-text);
    font-variant-numeric: tabular-nums;
}

.report-noi-row td:first-child {
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* Net Cash Flow row (final result) */
.report-ncf-row {
    background: linear-gradient(90deg, rgba(87, 131, 65, 0.45) 0%, rgba(87, 131, 65, 0.35) 100%) !important;
    border-top: 4px solid rgba(68, 101, 52, 1) !important;
    border-bottom: 4px solid rgba(68, 101, 52, 1) !important;
}

.report-ncf-row td {
    padding: 18px 12px !important;
    font-weight: 800;
    font-size: 1.1rem;
    color: #a5d98a;
    font-variant-numeric: tabular-nums;
}

.report-ncf-row td:first-child {
    text-transform: uppercase;
    letter-spacing: 1.2px;
}

/* Spacer rows */
.report-spacer-row {
    height: 40px;
    background: transparent !important;
}

.report-spacer-row td {
    border: none !important;
    padding: 0 !important;
}

/* Property breakdown tooltip indicator */
.property-breakdown-indicator {
    color: var(--prism-accent-text);
    font-size: 11px;
    margin-left: 4px;
    opacity: 0.8;
}

/* White content mode overrides */


.white-content .report-section-header {
    background: linear-gradient(90deg, rgba(var(--prism-accent-rgb), 0.1) 0%, transparent 100%) !important;
}

.white-content .report-grandtotal-row td {
    color: #d68745;
}

.white-content .report-ncf-row td {
    color: #5a8f41;
}

/* ===== REPORT CONTROLS ===== */
/* Modern control panel for report filters */
.report-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    justify-content: center;
    padding: 20px;background: var(--prism-surface-2);
    border-radius: 10px;
    margin-bottom: 20px;
}

.report-control-group {
    display: flex;
    align-items: center;
    gap: 8px;
}

.report-control-label {
    font-size: 0.9rem;color: var(--prism-text-muted);
    font-weight: 500;
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ===== MODERN DROPDOWN STYLING ===== */
/* Bootstrap-select dropdown modernization */
.report-controls .bootstrap-select .dropdown-toggle {background: var(--prism-tint-2) !important;
    background-image: none !important;border: 1px solid var(--prism-accent-border);
    border-radius: 8px;color: var(--prism-text);
    padding: 10px 16px;
    font-size: 0.95rem;
    transition: all 0.3s ease;
    min-width: 200px;
}

.report-controls .bootstrap-select .dropdown-toggle:hover,
.report-controls .bootstrap-select .dropdown-toggle:focus {background: var(--prism-tint-4) !important;
    background-image: none !important;border-color: var(--prism-accent-border);
    box-shadow: 0 0 0 3px rgba(var(--prism-accent-rgb), 0.1);
    color: #ffffff;
}

.report-controls .bootstrap-select .dropdown-toggle .filter-option {color: var(--prism-text);
}

.report-controls .bootstrap-select .dropdown-menu {background: var(--prism-surface-raised) !important;border: 1px solid var(--prism-accent-border);
    border-radius: 8px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
    margin-top: 8px;
}

.report-controls .bootstrap-select .dropdown-menu li a {color: var(--prism-text-2) !important;
    padding: 10px 16px;
    transition: all 0.2s ease;
}

.report-controls .bootstrap-select .dropdown-menu li a:hover {background: var(--prism-tint-4) !important;color: var(--prism-text) !important;
}

.report-controls .bootstrap-select .dropdown-menu li.selected a {
    background: rgba(var(--prism-accent-rgb), 0.25) !important;
    color: var(--prism-accent-text) !important;
}

.report-controls .bootstrap-select .dropdown-menu li.active a {
    background: rgba(var(--prism-accent-rgb), 0.25) !important;
    color: var(--prism-accent-text) !important;
}

.report-controls .bootstrap-select .bs-searchbox .form-control {background: var(--prism-surface-2);border: 1px solid var(--prism-accent-border);color: var(--prism-text);
    border-radius: 6px;
    padding: 8px 12px;
}

.report-controls .bootstrap-select .bs-searchbox .form-control:focus {background: var(--prism-surface-hover);border-color: var(--prism-accent-border);
    outline: none;
}

.report-controls .bootstrap-select .bs-searchbox .form-control::placeholder {color: var(--prism-text-muted);
}

/* White content mode overrides for dropdowns */

.white-content .report-controls .bootstrap-select .dropdown-menu {
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

.white-content .report-controls .bootstrap-select .dropdown-menu li.selected a,
.white-content .report-controls .bootstrap-select .dropdown-menu li.active a {
    background: rgba(var(--prism-accent-rgb), 0.2) !important;
    color: var(--prism-accent-text) !important;
}

/* ===== MODERN SELECT DROPDOWN ===== */
.prism-select-wrapper {
    position: relative;
    width: 100%;
}

.prism-select-wrapper::after {
    content: '';
    position: absolute;
    right: 16px;
    top: 50%;
    transform: translateY(-50%);
    width: 0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 8px solid var(--prism-accent);
    pointer-events: none;
    transition: all 0.3s ease;
    z-index: 1;
}

.prism-select-wrapper:hover::after {
    border-top-color: var(--prism-accent-light);
}

.prism-select-wrapper:focus-within::after {
    transform: translateY(-50%) rotate(180deg);
    border-top-color: var(--prism-accent-light);
}

.prism-select {
    font-size: 1.05em;
    font-weight: 500;border: 2px solid var(--prism-accent-border);background: var(--prism-tint-1);
    padding: 12px 45px 12px 20px;
    border-radius: var(--prism-radius-pill);
    width: 100%;color: var(--prism-text);
    transition: all 0.3s ease;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
    outline: none;
    /* The native POPUP follows this, not the option rules below — on macOS
       the menu is OS-drawn and ignores them, so without the scheme it opened
       light over a dark page. */
    color-scheme: var(--prism-color-scheme, dark);
}

.prism-select:hover {border-color: var(--prism-accent-border);background: var(--prism-tint-2);
}

.prism-select:focus {
    border-color: var(--prism-accent);background: var(--prism-tint-3);
    box-shadow: 0 5px 20px rgba(var(--prism-accent-rgb), 0.25);
}

.prism-select option {background: var(--prism-surface-raised);color: var(--prism-text);
    padding: 12px;
}

.white-content .prism-select:focus {
    box-shadow: 0 3px 15px rgba(var(--prism-accent-rgb), 0.15);
}

/* ===== STATS METRIC CARD STYLES ===== */
/* Used in: investment-stats.html */

.stats-page {
    font-family: var(--prism-font-body);
}

.stats-page .prism-card,
.stats-page .prism-card-glow {
    border-radius: 16px;
}

.stats-hero {
    position: relative;
    overflow: hidden;
    border: 1px solid rgba(var(--prism-accent-rgb), 0.35);
}

.stats-hero::before {
    content: "";
    position: absolute;
    inset: -40% -20% auto auto;
    width: 420px;
    height: 420px;
    background: radial-gradient(circle, rgba(var(--prism-accent2-rgb), 0.18) 0%, rgba(var(--prism-accent2-rgb), 0) 70%);
    pointer-events: none;
}

.stats-hero::after {
    content: "";
    position: absolute;
    inset: auto auto -120px -100px;
    width: 340px;
    height: 340px;
    background: radial-gradient(circle, rgba(var(--prism-accent-rgb), 0.2) 0%, rgba(var(--prism-accent-rgb), 0) 70%);
    pointer-events: none;
}

.stats-hero > .row {
    position: relative;
    z-index: 1;
}

.stats-hero-kicker {
    text-transform: uppercase;
    letter-spacing: 0.16em;
    font-size: 0.68rem;
    font-weight: 700;
    color: var(--prism-accent-text);
}

.stats-hero-title {
    font-family: var(--prism-font-display);
    font-size: 2.4rem;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: 0;
}

.stats-hero-subtitle {
    color: #c3d0e8;
    font-size: 1.02rem;
    max-width: 58ch;
}

/* The subtitle's one link. A bare <a> here inherits the vendor's purple, which
   measures 2.83:1 on the light hero — tests/audit_contrast.py caught it. The
   token is mode-aware and reads in both. */
.stats-hero-subtitle a {
    color: var(--prism-accent-text);
    text-decoration: underline;
}

.stats-hero-badges {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}

.stats-hero-spotlight {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 18px;
    border-radius: 14px;
    background: linear-gradient(155deg, rgba(var(--prism-accent-rgb), 0.2) 0%, rgba(var(--prism-accent-rgb), 0.06) 100%);border: 1px solid var(--prism-accent-border);
    box-shadow: 0 16px 32px rgba(3, 10, 30, 0.28);
}

.stats-hero-spotlight-label {
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 0.66rem;
    color: #9cb7df;
    font-weight: 700;
}

.stats-hero-spotlight-value {
    font-family: var(--prism-font-display);
    font-size: 1.8rem;
    line-height: 1;
    font-weight: 700;color: var(--prism-text);
}

.stats-hero-spotlight-footnote {
    font-size: 0.82rem;
    color: var(--prism-text-muted);
}

.stats-metric-icon {
    width: 44px;
    height: 44px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.15rem;
    margin-bottom: 0;
    flex-shrink: 0;background: var(--prism-tint-3);
    color: var(--prism-accent-text);border: 1px solid var(--prism-accent-border);
    transition: all 0.3s ease;
}

.prism-card:hover .stats-metric-icon,
.prism-card-glow:hover .stats-metric-icon {
    transform: scale(1.1);
}

.stats-metric-icon.icon-success {background: var(--prism-success-wash);color: var(--prism-success);
    border-color: rgba(0, 242, 195, 0.25);
}

.white-content .stats-metric-icon.icon-success {
    border-color: rgba(0, 242, 195, 0.15);
}

.stats-metric-icon.icon-danger {background: var(--prism-danger-wash);
    color: var(--prism-danger);
    border-color: rgba(253, 93, 147, 0.25);
}

.white-content .stats-metric-icon.icon-danger {
    border-color: rgba(253, 93, 147, 0.15);
}

.stats-metric-icon.icon-warning {background: var(--prism-warning-wash);
    color: var(--prism-warning);border-color: var(--prism-warning-border);
}

.stats-metric-label {
    font-size: 0.76rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1px;color: var(--prism-text-muted);
    margin-bottom: 8px;
    line-height: 1.4;
}

/* The card's "what does this include" trigger. It became a real control when
   the dashboard's portfolio band swapped `title=` for the shared
   `.stats-perf-tip` panel — focusable, hoverable, and therefore something a
   pointer has to be able to land on. As a bare <i> it took its whole size from
   the Font Awesome glyph, which is the trap the drawer's rename pencil already
   recorded: with the font unloaded it is a 0x0 target that still reports as
   visible. An explicit box and a focus ring, so the keyboard path is visible
   as well as reachable. */
.stats-metric-hint {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    vertical-align: -3px;
    cursor: help;
    border-radius: 50%;
}

.stats-metric-hint:hover {
    color: var(--prism-text);
}

.stats-metric-hint:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 1px;
}

/* The same job as .stats-metric-hint one rail up, on a badge instead of a card
   label — and the trade is the other way round, which is why it is a second
   class and not a reuse.

   There the <i> IS the trigger, so it needs an explicit 18px box or an
   unloaded Font Awesome leaves a 0x0 target that `is_visible` still reports as
   visible (the drawer rename-pencil trap). Here the BADGE is the trigger: it
   already has a hit box made of its own text and padding, so the glyph is pure
   decoration, carries `aria-hidden`, and an unloaded font costs the affordance
   and never the target. Sizing it would only make the pill taller.

   No `display`. Every .prism-badge-* declares `display: inline-block` at the
   same specificity, so an override here would win on source order alone — the
   `.stats-section-divider span` class of accident. It would buy nothing
   either: .stats-hero-badges is a flex container, so these badges are flex
   items and already blockified.

   No colour on the glyph. It inherits the badge's own, which keeps it legible
   on the warning wash and the info tint alike and adds no new colour pair for
   tests/audit_contrast.py to weigh. `prism-text-muted` — right for the cards,
   which sit on the card ground — would be a new pair on a tint here.

   The focus ring is not decoration: #portfolio-realized has shipped
   `tabindex="0"` with no focus style at all, so its keyboard path existed and
   was invisible. */
.stats-badge-hint {
    cursor: help;
}

.stats-badge-hint:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 2px;
}

.stats-badge-hint-icon {
    margin-left: 5px;
    vertical-align: -1px;
    /* Secondary to the figure, but the point of the glyph is to be FOUND —
       this badge's panel is the only thing on the rail that says which basis
       the number is in. Dimmer than this and the affordance is decorative. */
    opacity: 0.75;
}

.stats-badge-hint:hover .stats-badge-hint-icon,
.stats-badge-hint:focus-visible .stats-badge-hint-icon {
    opacity: 1;
}

.stats-metric-value {
    font-size: 1.45rem;
    font-weight: 800;color: var(--prism-text);
    line-height: 1.1;
    letter-spacing: -0.5px;
}

.stats-appreciation-row {
    display: flex;
    align-items: flex-end;
    gap: 14px;
    flex-wrap: wrap;
}

.stats-appreciation-pct {
    font-size: 1.45rem;
    font-weight: 800;
    line-height: 1.1;
    letter-spacing: -0.5px;
}

.stats-appreciation-amount {
    font-size: 0.95rem;
    font-weight: 600;color: var(--prism-text-muted);
    padding-bottom: 3px;
}

.stats-section-divider {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    position: relative;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--prism-accent-text);
    margin-bottom: 20px;
    padding-bottom: 12px;border-bottom: 1px solid var(--prism-accent-border);
}

/* DIRECT child. As a descendant selector this is (0,1,1) and beat every badge
   class inside the rail — the dashboard's portfolio band puts .prism-badge-* in
   here, and .prism-badge-primary lost its gradient to this background and fell to
   3.41:1 white-on-tint. Every other user of this divider has exactly one span,
   its own label, so `>` changes nothing for them. */
.stats-section-divider > span {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 11px;
    border-radius: 999px;background: var(--prism-tint-3);border: 1px solid var(--prism-accent-border);
}

/* Card-shaped, so it takes the card's material. It used to paint a near-opaque
   dark literal, rgba(7,17,37,.7 -> .35), which is why it needed a .white-content
   rule underneath purely to undo itself in light mode — and why that rule had
   to hand-copy the card gradient to stay in step. Both problems are the same
   problem: a literal where a token belongs. */
.stats-filter-bar {
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-card);
    padding: 20px 22px;
    margin-bottom: 35px;
    box-shadow: 0 14px 28px rgba(3, 11, 28, 0.22);
}

.stats-filter-label {
    text-transform: uppercase;
    letter-spacing: 0.09em;
    font-size: 0.73rem;
    font-weight: 700;color: var(--prism-accent-text);
}

.stats-filter-hint {
    margin-top: 5px;
    font-size: 0.82rem;
    /* Was #97a9cb with a #5e6f8e twin in light — two literals doing what one
       token does. */
    color: var(--prism-text-muted);
}

/* The background override that used to live here is gone: the base rule now
   reads the veil tokens, which flip on their own. Only the shadow remains, and
   that is a real asymmetry — dark lifts with a deep shadow, light with a
   shallow one. */
.white-content .stats-filter-bar {
    box-shadow: 0 10px 22px rgba(29, 37, 59, 0.08);
}

.stats-filter-bar .bootstrap-select > .dropdown-toggle {
    min-height: 42px;
    border-radius: 10px;border: 1px solid var(--prism-accent-border);background: var(--prism-tint-3);color: var(--prism-text);
}

.stats-filter-bar .bootstrap-select .filter-option-inner-inner {
    font-size: 0.9rem;
}

.stats-filter-bar .bootstrap-select .dropdown-menu {
    border-radius: 10px;
}

.stats-metric-card {
    min-height: auto;
    padding: 18px;
}

.stats-visual-subtitle {
    font-size: 0.8rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.stats-map-container {
    height: 420px;
    border-radius: 0 0 12px 12px;
    overflow: hidden;
    margin: -25px -25px -25px -25px;
}

.stats-map-container #map {
    width: 100%;
    height: 100%;
}

.stats-reveal {
    animation: stats-fade-up 0.55s ease;
}

.stats-metric-grid .col-xl-3:nth-child(1),
.stats-metric-grid .col-lg-4:nth-child(1) {
    animation: stats-fade-up 0.35s ease;
}

.stats-metric-grid .col-xl-3:nth-child(2),
.stats-metric-grid .col-lg-4:nth-child(2) {
    animation: stats-fade-up 0.45s ease;
}

.stats-metric-grid .col-xl-3:nth-child(3),
.stats-metric-grid .col-lg-4:nth-child(3) {
    animation: stats-fade-up 0.55s ease;
}

.stats-metric-grid .col-xl-3:nth-child(4) {
    animation: stats-fade-up 0.65s ease;
}

@keyframes stats-fade-up {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* --- Asset Performance map + table (investment-stats.html) ---------------
   The card that replaced the scrolling dual-axis bar chart. The chart half is
   Highcharts and styles itself from PrismHighcharts.palette(); everything here
   dresses the table half and the toolbar.

   The toolbar reuses .perf-toolbar / .perf-tool-btn / .perf-tool-select from
   the ASSET PERFORMANCE section above unchanged — those were already the
   app's sort-and-filter chrome and there is no reason for a second set. */

/* Sortable column header. Named neutrally rather than .stats-perf-* because it
   is not specific to this page: categories.html carries a page-local .led-sort
   that is the same component down to the ▲/▼ indicator, and should adopt this
   rather than a third copy being written for the next table. */
.prism-sort {
    cursor: pointer;
    user-select: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    transition: color 0.15s ease;
}

.prism-sort:hover,
.prism-sort.active {
    color: var(--prism-accent-text);
}

.prism-sort:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 2px;
    border-radius: 3px;
}

.prism-sort-ind {
    font-size: 0.66em;
    line-height: 1;
}

.prism-sort-ind.active {
    color: var(--prism-accent-text);
}

/* The table scrolls, and that is fine — a long table is expected to. It is the
   CHART that must not, which is why its height is fixed and asset-count
   independent.

   Both axes are contained here, and both need help being FOUND. The overflow
   was always inside the container, but with the platform's overlay scrollbars
   there was no chrome to see and no shadow at the edge, so a table clipped
   mid-column at "MORTGAGE & LOA…" looked like a rendering bug rather than
   something you could scroll. `overscroll-behavior` keeps a flick at the end
   of the list from scrolling the page behind it. */
.stats-perf-table-scroll {
    max-height: 460px;
    overflow: auto;
    overscroll-behavior: contain;
}

/* The header row and the asset name stay put. With eight columns you could
   still tell which row you were on after scrolling right; with the exit set
   you cannot, and a number with no asset beside it is not information. */
/* --prism-surface-raised, NOT --prism-surface-2. Both read as "the table's own
   surface" in review and only one of them is opaque: surface-2 is
   rgba(255,255,255,0.03), so a frozen cell painted with it lets the columns
   sliding underneath show straight through, and the asset name and the
   mortgage column render on top of each other. Raised is the token the app
   already uses for chrome that floats over arbitrary content (tooltips,
   dropdowns), which is exactly what a frozen row or column is. */
.stats-perf-table thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--prism-surface-raised);
}

/* Above 1, and that is the whole point: .stats-perf-cell already carries
   `z-index: 1`, and its td does not create a stacking context, so those spans
   compete directly with this one. At an equal z-index the later element in
   DOM order wins — every scrolling column comes after the asset cell — so a
   frozen column at `z-index: 1` is opaque, correctly positioned, and still has
   dollar figures rendered straight across the asset names. */
.stats-perf-table .stats-perf-sticky {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--prism-surface-raised);
}

/* The corner cell is both, so it has to outrank each. */
.stats-perf-table thead th.stats-perf-sticky {
    z-index: 4;
}

/* Opaque, and it has to be: a sticky cell is painted OVER the cells sliding
   under it, and the table's own translucent surface would let them show
   through. The row tints below reassert themselves on top of this, or a
   hovered row would break at the frozen column. */
.stats-perf-table tbody tr:hover .stats-perf-sticky,
.stats-perf-table tbody tr.is-linked .stats-perf-sticky,
.stats-perf-table tbody tr.is-drilled .stats-perf-sticky {
    background-image: linear-gradient(var(--prism-tint-2), var(--prism-tint-2));
}

/* An edge people can see. Pure decoration is exactly what this is not — it is
   the only cue that the frozen column has content sliding beneath it. */
.stats-perf-table .stats-perf-sticky::after {
    content: '';
    position: absolute;
    inset: 0 -1px 0 auto;
    width: 1px;
    background: var(--prism-border);
}

/* A <figure> carries `margin: 1em 40px` from the UA sheet, which indents the
   plot inside the card and pads dead space under it. */
.stats-visual-card .highcharts-figure {
    margin: 0;
}

/* The Asset header was centred over a left-aligned column — `th` centres by
   default and .report-table does not say otherwise. */
.stats-perf-table thead th:first-child .prism-sort {
    justify-content: flex-start;
}

.stats-perf-table thead th:first-child {
    text-align: left;
}

.stats-perf-table thead th {
    padding: 12px;
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    white-space: nowrap;
}

.stats-perf-table th.stats-perf-num .prism-sort {
    justify-content: flex-end;
}

.stats-perf-table th.stats-perf-num,
.stats-perf-table td.stats-perf-num {
    text-align: right;
}

.stats-perf-table tbody td {
    padding: 9px 12px;
    font-size: 0.85rem;
    white-space: nowrap;
}

.stats-perf-table tbody tr:hover,
.stats-perf-table tbody tr.is-linked {
    background: var(--prism-tint-2);
}

/* In-cell magnitude bar. This is what makes the sorted table the ranked bar
   chart the old view was reaching for, without a second chart: the column you
   sorted by reads as bars top-to-bottom. --perf-bar-w is written by JS because
   the width is DATA; the colour stays here because it is theme, and setting it
   from JS would put it out of light mode's reach while still looking right. */
.stats-perf-bar {
    position: relative;
}

.stats-perf-bar::before {
    content: '';
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    height: 20px;
    width: var(--perf-bar-w, 0%);
    border-radius: 4px;
    background: rgba(var(--prism-accent-rgb), 0.16);
    pointer-events: none;
}

.stats-perf-bar.is-negative::before {
    background: rgba(var(--prism-danger-rgb), 0.18);
}

.stats-perf-cell {
    position: relative;
    z-index: 1;
}

/* Tooltip body. Values lead and labels follow — inverted from the legend,
   because here the reader has the asset and wants the number. */
.stats-perf-tip-name {
    display: block;
    font-weight: 700;
    margin-bottom: 4px;
}

.stats-perf-tip-row {
    display: flex;
    justify-content: space-between;
    gap: 18px;
    font-size: 0.8rem;
}

.stats-perf-tip-label {
    color: var(--prism-text-muted);
}

/* The break-even tooltip's cost-basis and spend-since rows are the two parts
   of the Money spent row above them, so they sit indented under it. Owned here
   rather than borrowed from the vendor sheet's .pl-3: prism-common.css is the
   only stylesheet that card's browser tests load, so a borrowed utility paints
   nothing there while a class-name assertion still passes. */
.stats-perf-tip-row.is-part {
    padding-left: 14px;
}

/* A plain-words gloss under a tooltip's numbers. The Total Return waterfall
   needs it because a bar's NAME does not explain it there — "Purchase price"
   drawn upward reads as a gain until you know it is what the cash and the debt
   together bought. */
.stats-perf-tip-note {
    display: block;
    margin-top: 4px;
    max-width: 230px;
    font-size: 0.72rem;
    line-height: 1.35;
    color: var(--prism-text-muted);
    white-space: normal;
}

/* A break-even price that sits UNDER the loan balance. The number is correct —
   the deal has already returned more than it cost, so selling there leaves the
   investment whole — but the sale does not fund its own payoff and needs a
   cheque at closing. The tooltip carries the amount; this is the cue that there
   is something to hover.

   --prism-accent and deliberately NOT --prism-warning or --prism-danger. This
   is the state a refinance is TRYING to reach, and on the portfolio it was
   written against it is 8 of 33 rows — painting the best-performing assets on
   the page like a problem would misread them. The marker says "there is a
   caveat here", not "something is wrong".

   On .stats-perf-cell rather than the td: the td already paints the magnitude
   bar in ::before, and a border there would draw across the bar instead of
   under the figure. */
.stats-perf-table td.is-below-payoff .stats-perf-cell {
    border-bottom: 1px dashed var(--prism-accent);
    padding-bottom: 1px;
}

/* The TABLE's tooltip — the shell only; every row inside it is one of the
   .stats-perf-tip-* classes above, shared with the two Highcharts tooltips on
   this page.

   It exists because the table used to borrow the CHART's tooltip, and setView
   hides the chart whenever the table is on screen: Highcharts positions an
   `outside` tooltip off the container's rect, a display:none container's rect
   is collapsed, so it rendered under the toolbar however far down the table
   the cursor was.

   Mounted on <body> and `position: fixed` so .stats-perf-table-scroll's
   `overflow: auto` cannot clip it. Values match prism-highcharts.js's own
   tooltip theme — the same reader meets both on this page and they should not
   look like two components. `pointer-events: none` is load-bearing: a row
   click drills the trend card below, and a panel parked under the cursor
   would eat it. */
.stats-perf-tip {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10600;
    padding: 14px;
    border-radius: 12px;
    background: var(--prism-surface-raised);
    border: 1px solid var(--prism-accent-border);
    color: var(--prism-text);
    font-size: 13.5px;
    line-height: 1.55;
    pointer-events: none;
}

/* `hidden` is a UA-level rule and loses to any author selector that sets
   display. Neither view container has one today; those two are here so adding
   one later cannot silently break the view toggle. The tooltip's is not
   defensive — it is the only thing that hides it. */
.stats-perf-tip[hidden],
#perf-view-map[hidden],
#perf-view-table[hidden] {
    display: none;
}

.perf-tool-btn.is-active {
    background: rgba(var(--prism-accent-rgb), 0.18);
    border-color: var(--prism-accent);
    color: var(--prism-text);
}

/* --- Break-even Progress (investment-stats.html) -------------------------
   The trend card under the performance card: the cumulative earned-vs-spent
   race to break-even. The chart styles itself from PrismHighcharts.palette();
   this dresses the headline strip, the drill-scope chip, and the rows above
   that drive it. Toolbar chrome is .perf-toolbar / .perf-tool-btn, unchanged. */

/* Rows in the performance table drill the trend card below, so they are click
   targets now, and the drilled one stays marked while the trend shows it. */
.stats-perf-table tbody tr {
    cursor: pointer;
}

.stats-perf-table tbody tr.is-drilled {
    background: var(--prism-tint-3);
    box-shadow: inset 3px 0 0 var(--prism-accent);
}

/* One view left, so the drill-scope chip is the whole toolbar and right-aligns
   itself — there is no longer a view group opposite it for space-between to
   push against. */
#trend-toolbar {
    justify-content: flex-end;
}

.trend-headline {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 10px 28px;
    margin: 2px 0 16px;
}

/* `hidden` is UA-level and loses to any author display — the same guard the
   perf view containers carry above. Without these the JS sets the attribute
   and nothing moves. #trend-toolbar is on the list because .perf-toolbar sets
   display:flex, and with the view buttons gone the toolbar holds nothing but
   the drill-scope chip — an empty flex row with an 18px margin under it. */
.trend-headline[hidden],
#trend-toolbar[hidden],
#trend-scope[hidden],
#trend-view-chart[hidden] {
    display: none;
}

.trend-headline-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.trend-headline-label {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--prism-text-muted);
}

.trend-headline-value {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--prism-text);
    font-variant-numeric: tabular-nums;
}

/* The two parts of Money spent, under the total they add up to — the same
   subordination the tooltip's .is-part rows get, expressed as size and weight
   because a headline stat is a column and has no row to indent. */
.trend-headline-detail {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--prism-text-muted);
    font-variant-numeric: tabular-nums;
}

.trend-scope-name {
    background: var(--prism-tint-2);
    border: 1px solid var(--prism-accent-border);
    border-radius: 8px;
    padding: 4px 10px;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--prism-text);
}

.trend-empty {
    padding: 34px 12px;
    text-align: center;
    font-size: 0.85rem;
}

/* Under-chart honesty line — which assets the numbers above could not fully
   measure. Deliberately not styled as a warning: it is a statement about the
   data, and dressing it in danger red would read as the assets being in
   trouble rather than as the record being incomplete. */
.trend-disclosure {
    margin: 10px 0 0;
    font-size: 0.75rem;
    line-height: 1.45;
}

.trend-disclosure[hidden],
#return-toolbar[hidden],
#return-scope[hidden],
#return-view-chart[hidden] {
    display: none;
}

#return-toolbar {
    justify-content: flex-end;
}

.white-content .stats-hero-subtitle {
    color: #4f6488;
}

.white-content .stats-hero-spotlight {
    background: linear-gradient(155deg, rgba(var(--prism-accent-rgb), 0.14) 0%, rgba(var(--prism-accent-rgb), 0.05) 100%);
    box-shadow: 0 10px 24px rgba(29, 37, 59, 0.1);
}

.white-content .stats-hero-spotlight-label {
    color: #5579aa;
}

@media (max-width: 991px) {
    .stats-hero-title {
        font-size: 2rem;
    }

    .stats-hero-subtitle {
        max-width: 100%;
    }

    .stats-map-container {
        height: 360px;
    }
}

@media (max-width: 576px) {
    .stats-hero-title {
        font-size: 1.7rem;
    }

    .stats-hero-badges {
        gap: 8px;
    }

    .stats-filter-bar {
        padding: 16px;
    }

    .stats-map-container {
        height: 320px;
    }
}

/* ===== CHART FULL WIDTH STYLING ===== */
/* Ensure chart takes full width of container */
#cash-flow-chart {
    width: 100%;
    margin: 0 -25px;
    padding: 0;
}

#cash-flow-chart .highcharts-figure {
    width: 100%;
    margin: 0;
}

#cash-flow-chart #container {
    width: 100% !important;
    height: 500px;
}

/* ===== CATEGORY SELECTION UI WITH EMOJIS ===== */
/* Grid layout for category options */
.category-options-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 10px;
    padding: 4px;
}

/* Individual category option card */
.category-option {background: var(--prism-surface-2);
    border: 2px solid rgba(255, 255, 255, 0.15);
    border-radius: 12px;
    padding: 14px 12px;
    cursor: pointer;
    transition: all 0.3s ease;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    text-align: center;
    position: relative;
    min-height: 90px;
    justify-content: center;
}

.category-option:hover {background: var(--prism-tint-4);
    border-color: rgba(var(--prism-accent-rgb), 0.4);
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(var(--prism-accent-rgb), 0.2);
}

.category-option.selected {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.25) 0%, rgba(var(--prism-accent-rgb), 0.15) 100%);
    border-color: var(--prism-accent);
    box-shadow: 0 4px 16px rgba(var(--prism-accent-rgb), 0.35);
}

.category-option.selected::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 10px;
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.1) 0%, transparent 100%);
    pointer-events: none;
}

/* Category emoji display */
.category-emoji {
    font-size: 2em;
    line-height: 1;
    margin-bottom: 4px;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
    transition: transform 0.3s ease;
}

.category-option:hover .category-emoji {
    transform: scale(1.15);
}

.category-option.selected .category-emoji {
    transform: scale(1.1);
    filter: drop-shadow(0 4px 8px rgba(var(--prism-accent-rgb), 0.4));
}

/* Category name text */
.category-name {
    font-size: 0.85em;
    font-weight: 600;color: var(--prism-text-2);
    line-height: 1.3;
    word-break: break-word;
}

.category-option:hover .category-name {color: var(--prism-text);
}

.category-option.selected .category-name {color: var(--prism-accent-text);
}

/* Check icon for selected category */
.category-check {
    position: absolute;
    top: 8px;
    right: 8px;color: var(--prism-success);
    font-size: 1em;background: var(--prism-success-wash);
    border-radius: 50%;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: checkBounce 0.4s ease-out;
}

@keyframes checkBounce {
    0% {
        transform: scale(0);
        opacity: 0;
    }
    50% {
        transform: scale(1.2);
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

/* White content mode overrides */
.white-content .category-option {
    border: 2px solid rgba(var(--prism-accent-rgb), 0.2);
}

.white-content .category-option.selected {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.2) 0%, rgba(var(--prism-accent-rgb), 0.1) 100%);
    box-shadow: 0 4px 16px rgba(var(--prism-accent-rgb), 0.25);
}

/* Responsive adjustments for smaller screens */
@media (max-width: 480px) {
    .category-options-grid {
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: 8px;
    }

    .category-option {
        padding: 12px 10px;
        min-height: 80px;
    }

    .category-emoji {
        font-size: 1.6em;
    }

    .category-name {
        font-size: 0.8em;
    }
}

/* ===== REDESIGNED CATEGORY EDIT INTERFACE ===== */

/* Main container for category edit */
.category-edit-container {
    display: flex;
    flex-direction: column;
    height: 100%;
    gap: 20px;
}

/* Unified container for all sidebar transaction views */
.sidebar-view-container {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-height: 100%;
}

/* Sticky-footer variant: middle section scrolls, footer stays pinned. Used by
   the transaction-detail view so the action buttons remain reachable when the
   Related Activity list pushes content beyond the viewport. */
.sidebar-view-container--sticky-footer {
    min-height: 0;
    height: 100%;
    gap: 0;
    overflow: hidden;
}

.sidebar-view-container--sticky-footer .sidebar-view-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-right: 4px;
    margin-right: -4px;
}

.sidebar-view-container--sticky-footer > .sidebar-footer-actions {
    flex-shrink: 0;
    margin-top: 16px;
}

/* Subtle styled container for split allocation rows */
.split-allocation-row {
    background: rgba(139, 92, 246, 0.05);
    padding: 12px;
    border-radius: 8px;
    margin-bottom: 10px;
    border-left: 3px solid #8b5cf6;
}

/* The amount/note inputs are Bootstrap .form-control, whose default :focus
   glow is blue. Re-tone it to the prism accent so the split form matches the
   rest of the skin (the category picker's search field, etc.). */
.split-allocation-row .form-control:focus {
    border-color: var(--prism-accent);
    box-shadow: 0 0 0 0.2rem rgba(var(--prism-accent-rgb), 0.25);
}

/* The last row auto-balances: its amount is computed, not typed. Dim it and
   tint it with the accent so it reads as "filled for you", not editable. */
.split-allocation-row .split-amount[readonly] {
    cursor: default;
    opacity: 0.85;
    border-color: rgba(var(--prism-accent-rgb), 0.35);
    background: rgba(var(--prism-accent-rgb), 0.06);
}

/* Transaction Header Card */
.transaction-header-card {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.08) 0%, rgba(var(--prism-accent-rgb), 0.02) 100%);border: 1px solid var(--prism-accent-border);
    border-radius: 16px;
    padding: 20px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.transaction-header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
}

.transaction-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.transaction-date {
    font-size: 0.85em;color: var(--prism-text-dim);
    font-weight: 500;
}

.transaction-name {
    font-size: 1.1em;
    font-weight: 600;color: var(--prism-text);
    line-height: 1.3;
}

.transaction-type {
    margin-top: 4px;
}

.badge-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 0.75em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.badge-cash {
    background: rgba(255, 191, 0, 0.2);
    color: #ffbf00;
    border: 1px solid rgba(255, 191, 0, 0.4);
}

.badge-bank {
    background: rgba(var(--prism-accent-rgb), 0.2);
    color: var(--prism-accent-text);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.4);
}

/* Per-payment-method pill (Cash / Apple Pay / Zelle / etc). Inline brand
   color is set by the payment_method_badge macro; this rule sizes the icon
   so emoji and letter glyphs (G, $) render at the badge cap-height. */
.payment-method-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1em;
    line-height: 1;
    font-size: 1em;
    font-weight: 700;
}

.transaction-amount {
    font-size: 1.5em;
    font-weight: 700;
    text-align: right;
    line-height: 1;
}

.transaction-amount.positive {
    color: var(--prism-success);
    text-shadow: 0 2px 8px rgba(0, 242, 195, 0.3);
}

.transaction-amount.negative {
    color: var(--prism-danger);
    text-shadow: 0 2px 8px rgba(253, 93, 147, 0.3);
}

/* Transaction detail (right sidebar) layout helpers
   Composes existing prism tokens (.badge-pill, .prism-card-subtle, .prism-badge-*).
   Only adds layout glue + the similar-transactions list rows. */
.tx-meta-row {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;color: var(--prism-text-dim);
    font-size: var(--prism-fs-sm);
}

.tx-meta-row > i.tim-icons {
    font-size: 1em;color: var(--prism-text-dim);
}

.tx-meta-bullet {color: var(--prism-text-faint);
}

/* Click-to-edit category cell. The trigger displays the existing category
   pill; clicking it opens a custom prism-styled dropdown listing every
   category as a colored cat-badge pill. Replaces the standalone "Edit
   Category" picker for the right-sidebar transaction-detail view. */
.tx-cat-editable {
    position: relative;
    display: inline-block;
    width: 100%;
    min-width: 0;
}
.tx-cat-trigger {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    border-radius: 999px;
    transition: transform 0.15s ease;
}
.tx-cat-trigger:hover,
.tx-cat-trigger:focus { outline: none; }
.tx-cat-trigger:hover .cat-badge,
.tx-cat-trigger:focus .cat-badge {
    transform: translateY(-1px);
}
.tx-cat-chevron {
    font-size: 0.6em !important;
    opacity: 0.5;
    transition: opacity 0.15s ease, transform 0.2s ease;
}
.tx-cat-trigger[aria-expanded="true"] .tx-cat-chevron {
    transform: rotate(180deg);
    opacity: 0.9;
}
.tx-cat-trigger:hover .tx-cat-chevron,
.tx-cat-trigger:focus .tx-cat-chevron { opacity: 0.9; }

/* The dropdown menu — prism dark/light card, absolute-positioned below the
   trigger, scrollable, with a search input on top. */
.tx-cat-menu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    width: max(100%, 280px);
    max-width: 360px;
    z-index: 1060;
    /* Floating chrome over arbitrary content, so it takes --prism-panel like
       every other dropdown rather than its own literal pair. */
    background: var(--prism-panel);border: 1px solid var(--prism-accent-border);
    border-radius: 12px;
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: tx-cat-menu-in 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes tx-cat-menu-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}
.white-content .tx-cat-menu {
    background: var(--prism-panel);
    box-shadow: 0 18px 40px rgba(29, 37, 59, 0.18);
}

.tx-cat-menu-search {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;border-bottom: 1px solid var(--prism-border-faint);
    background: rgba(255, 255, 255, 0.02);
}
.tx-cat-menu-search > i.tim-icons {color: var(--prism-text-faint);
    font-size: 0.95em;
}
.tx-cat-search-input {
    flex: 1;
    background: transparent;
    border: 0;
    outline: none;color: var(--prism-text);
    font-size: var(--prism-fs-sm);
}
.tx-cat-search-input::placeholder {color: var(--prism-text-faint); }
.white-content .tx-cat-menu-search {
    background: rgba(var(--prism-accent-rgb), 0.04);
}

.tx-cat-menu-scroll {
    max-height: 360px;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 6px 0;
}
.tx-cat-menu-scroll::-webkit-scrollbar { width: 8px; }
.tx-cat-menu-scroll::-webkit-scrollbar-track { background: transparent; }
.tx-cat-menu-scroll::-webkit-scrollbar-thumb {
    background: rgba(var(--prism-accent-rgb), 0.3);
    border-radius: 8px;
}
.tx-cat-menu-scroll::-webkit-scrollbar-thumb:hover { background: rgba(var(--prism-accent-rgb), 0.5); }

.tx-cat-group { padding: 4px 0; }
.tx-cat-group + .tx-cat-group {border-top: 1px solid var(--prism-border);
    margin-top: 2px;
    padding-top: 6px;
}

/* Group header doubles as a selectable option for the parent category.
   Renders like a section label but is fully clickable — hover gets a subtle
   tint matching its bucket color, and a check appears when the parent is
   the active category. */
.tx-cat-group-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 6px 14px;
    background: transparent;
    border: 0;
    cursor: pointer;
    text-align: left;
    font-size: var(--prism-fs-2xs);
    font-weight: 700;
    letter-spacing: 1.1px;
    text-transform: uppercase;color: var(--prism-text-dim);
    transition: background-color 0.12s ease;
}
.tx-cat-group-header:hover,
.tx-cat-group-header:focus {background: var(--prism-surface-2);
    outline: none;
}
.tx-cat-group-header.is-selected {background: var(--prism-tint-4);
}

.tx-cat-group[data-bucket="income"]  .tx-cat-group-header { color: #2dce89; }
.tx-cat-group[data-bucket="expense"] .tx-cat-group-header { color: var(--prism-danger); }
.tx-cat-group[data-bucket="income"]  .tx-cat-group-header:hover,
.tx-cat-group[data-bucket="income"]  .tx-cat-group-header:focus  { background: rgba(45, 206, 137, 0.12); }
.tx-cat-group[data-bucket="expense"] .tx-cat-group-header:hover,
.tx-cat-group[data-bucket="expense"] .tx-cat-group-header:focus  { background: rgba(253, 93, 147, 0.10); }

.tx-cat-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 7px 14px;
    background: transparent;
    border: 0;
    cursor: pointer;
    transition: background-color 0.12s ease;
    text-align: left;color: var(--prism-text);
    font-size: var(--prism-fs-base);
    font-weight: 500;
}
.tx-cat-option:hover,
.tx-cat-option:focus {background: var(--prism-tint-3);
    outline: none;
}
.tx-cat-option.is-selected {background: var(--prism-tint-4);
}
.tx-cat-option .cat-badge {
    /* Pill inside the dropdown is slightly tighter than a standalone pill */
    font-size: var(--prism-fs-xs);
    padding: 3px 8px;
}
.tx-cat-option-check {
    color: var(--prism-accent-text);
    font-size: 0.95em;
    flex-shrink: 0;
}

.tx-cat-empty {
    padding: 18px 14px;
    text-align: center;color: var(--prism-text-faint);
    font-size: var(--prism-fs-xs);
}

/* Click-to-edit affordance for Notes / Invoice fields in the right-sidebar
   transaction-detail view. The pencil button only renders when the user
   has Edit permission on the corresponding resource (server-side
   `can_edit_note` / `can_edit_invoice` flag). */
.tx-editable-field {
    display: block;
}
.tx-editable-header {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 4px;
}
.tx-editable-header .prism-field-label { margin: 0; }

/* The read-only half of an editable field — the transaction's Notes and
   Invoice as they look before the pencil is clicked.

   It had NO rule at all: every sibling in this block was defined and this one
   was not, so it took browser defaults. That was invisible for a URL and wrong
   for Notes, whose editor is a <textarea rows="3"> — you could type three
   paragraphs, save, and watch them come back as one. Same defect as the
   asset drawer's loan-event notes; see .sidebar-kv-row--stack above for why
   pre-wrap is the fix and why the escaping makes it safe.

   No `display` here on purpose: _renderEditableField toggles this element with
   `style.display = 'none'` / `= ''`, and the empty string falls back to
   whatever this rule says. A `display: flex` added later would be restored on
   every cancel. */
.tx-editable-view {
    font-size: var(--prism-fs-base);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.tx-editable-edit-btn {
    background: transparent;
    border: 0;
    padding: 2px;
    cursor: pointer;color: var(--prism-text-faint);
    border-radius: 4px;
    line-height: 1;
    transition: color 0.15s ease, background-color 0.15s ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.tx-editable-edit-btn:hover,
.tx-editable-edit-btn:focus {
    color: var(--prism-accent-text);background: var(--prism-tint-3);
    outline: none;
}
.tx-editable-edit-btn i.tim-icons { font-size: 0.85em; }

.tx-editable-editor {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.tx-editable-editor .tx-editable-input {
    width: 100%;
    /* Heavier inner padding than the default form-control: the input has a
       large border-radius (rounded "pill"-style corners), so the curved edge
       eats into the visible text region. Extra horizontal padding keeps the
       text clear of the curve; vertical padding gives the multiline textarea
       a comfortable line-height. */
    padding: 10px 14px;
}
.tx-editable-editor textarea.tx-editable-input {
    /* Textarea-specific: a touch more vertical room and prevent horizontal
       resize so the user can't drag past the rounded right edge. */
    padding: 12px 14px;
    resize: vertical;
}
.tx-editable-actions {
    display: flex;
    justify-content: flex-end;
    gap: 6px;
}
.tx-editable-actions .prism-btn-sm {
    padding: 4px 12px;
    font-size: var(--prism-fs-2xs);
    letter-spacing: 0.3px;
}

/* Revert sits at the far left of the action row, away from Cancel and Save:
   it is the one control there that discards what the user typed, and 6px from
   Cancel is a misclick. `justify-content: flex-end` puts everything right, so
   margin-right: auto is what pushes this one back out.

   The [hidden] restatement is the house rule for any element JS hides by
   attribute — `[hidden] { display: none }` is UA-level and loses to any class
   selector that sets `display`. Nothing on .prism-btn-* sets one today, so
   this changes nothing now; it is here so that adding `display: inline-flex`
   to a button rule later cannot silently strand a Revert button on a row with
   nothing to revert. */
.tx-editable-actions .tx-editable-revert {
    margin-right: auto;
}
.tx-editable-actions .tx-editable-revert[hidden] {
    display: none;
}

/* The drawer headline as an editable field — the transaction's display name,
   with the pencil beside it instead of above it.

   Two things here are load-bearing:

   The pencil stays in .tx-editable-header, OUTSIDE .tx-editable-view, and is
   moved next to the name with `order` alone. _closeEditableField rewrites the
   view's innerHTML on every save, so a pencil actually nested inside it would
   lose its click listener after the first rename and never open again.

   And nothing here gives .tx-editable-view a `display`. The comment on that
   rule above explains why: the JS toggles it with `style.display = ''`, which
   falls back to whatever the stylesheet says, so a `display` declared here
   would be restored on every cancel. Layout lives on the wrapper instead. */
.tx-editable-field--headline {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
    min-width: 0;
    flex: 1 1 auto;
}
.tx-editable-field--headline .tx-editable-header {
    order: 2;
    margin-bottom: 0;
}
.tx-editable-field--headline .tx-editable-view {
    order: 1;
    min-width: 0;
}
.tx-editable-field--headline .tx-editable-editor {
    order: 3;
    flex: 1 0 100%;
    margin-top: 8px;
}

/* While editing, the field takes a whole row of .tx-headline and the amount
   keeps the one above it. Without this the input shares its row with the
   amount and measures ~230px in a 400px drawer — narrow for the one field
   whose value is a merchant string, and narrowest exactly when the drawer is.
   `order` is what puts the amount on top rather than under the Save button;
   `margin-left: auto` keeps it right-aligned once it is alone on its line,
   where space-between has nothing left to distribute. */
.tx-headline {
    flex-wrap: wrap;
}
.tx-editable-field--headline.is-editing {
    flex: 1 1 100%;
    order: 2;
}
.tx-headline .transaction-amount {
    order: 1;
    margin-left: auto;
}

/* Row that pairs a small `prism-field-label` ("Category") with an inline origin
   pill ("Auto · AI", etc.). The pill sits at label-level so it annotates the
   field without competing with the value (the category pill) below it. */
.tx-pill-label-row {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    min-height: 18px;
}
.tx-pill-label-row > .prism-field-label {
    margin: 0;
}
/* An inline action button at label-level (e.g. the Tags "Add" button) should
   match the compact footer buttons, not the default full-size prism-btn-sm. */
.tx-pill-label-row .prism-btn-sm {
    padding: 6px 10px;
    font-size: var(--prism-fs-2xs);
    letter-spacing: 0.3px;
}
.tx-pill-label-row .prism-btn-sm i.tim-icons {
    font-size: 0.85em;
    margin-right: 3px;
}

/* Compact origin chip used in `.tx-pill-label-row`. Smaller than the value
   pill below, with a subtle tinted background so it's clearly visible
   without dominating the category. Color is set inline by JS based on the
   categorization source (rule / AI / legacy). */
.tx-origin-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.3px;
    text-transform: uppercase;
    line-height: 1.3;
    white-space: nowrap;
}
.tx-origin-chip > i.tim-icons {
    font-size: 0.85em;
}

.tx-headline {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
}

.tx-headline .transaction-name {
    font-size: var(--prism-fs-lg);
}

.tx-headline .transaction-amount {
    font-size: var(--prism-fs-xl);
}

/* Admin-only line beneath .tx-headline showing the raw Plaid name when it
   differs from the display name. Quiet styling so it reads as metadata. */
.tx-original-name {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-top: 4px;
    font-size: var(--prism-fs-xs);color: var(--prism-text-dim);
    font-style: italic;
}
.tx-original-name-label {
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.6px;
    text-transform: uppercase;color: var(--prism-text-faint);
    font-style: normal;
    flex-shrink: 0;
}
.tx-original-name-text {
    word-break: break-word;
    overflow-wrap: anywhere;
    min-width: 0;
}

.tx-pill-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
}

.sidebar-divider {
    border: 0;
    height: 1px;background: var(--prism-surface-hover);
    margin: 20px 0;
}

/* Says what the Related Activity list was matched on. Carries
   .prism-field-hint for its size and colour — the drawer already has a muted
   caption token and a second one would be a second thing to keep on the type
   scale. This rule only positions it under the centered section label, so it
   declares no font-size and no colour of its own. */
.tx-similar-basis {
    margin-top: 0;
    margin-bottom: 10px;
    text-align: center;
}

/* Similar transactions list (uses existing .badge-pill / .text-muted).
   Centered month-group header (name + total inline as a single unit) reads
   like a section divider — used in both Related Activity and the category
   drilldown so consecutive months read as distinct groups. */
.tx-similar-month-header {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 12px;
    margin-top: 18px;
    margin-bottom: 6px;
}

.tx-similar-month-name {
    font-size: var(--prism-fs-lg);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;color: var(--prism-text);
}

/* The comparison-mode picker drops over the chart panel, whose pink/purple
   bars crush contrast through the default tx-cat-menu's gradient. Scope a
   denser opaque backdrop + blur + stronger shadow so options stay readable.
   The transaction-detail category picker (which lives outside this shell)
   keeps the default lighter card styling. */
.category-transactions-shell .tx-cat-menu {
    background: #13162a;border-color: var(--prism-accent-border);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.04) inset;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
}
.white-content .category-transactions-shell .tx-cat-menu {
    background: var(--prism-surface-raised);
    box-shadow: 0 24px 60px rgba(29, 37, 59, 0.25);
}

/* The drilldown chart's y-axis labels — the median and the top of the scale.
   Highcharts moves the y axis into its fixed, non-scrolling layer, which is what
   lets these stay readable while the plot pans; the cost is that bars slide
   underneath them, and muted text on a bar is not legible. A chip in the drawer's
   own ground restores it without a heavy plate. One token, so light mode and all
   nine skins follow with no override. */
.drilldown-axis-label {
    background: var(--prism-drawer-ground);
    padding: 0 4px;
    border-radius: 4px;
    white-space: nowrap;
}

.tx-similar-month-total {
    font-size: var(--prism-fs-base);
    font-weight: 600;
}

.tx-similar-month-total.positive { color: var(--prism-success); }
.tx-similar-month-total.negative { color: var(--prism-danger); }

.tx-similar-day-label {
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.6px;color: var(--prism-text-dim);
    margin-top: 10px;
    margin-bottom: 4px;
}

.tx-similar-row {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) auto auto;
    gap: 10px;
    align-items: center;
    padding: 8px 0;border-bottom: 1px solid var(--prism-border-faint);
    font-size: var(--prism-fs-base);
}

.tx-similar-row:last-child {
    border-bottom: 0;
}

.tx-similar-row.is-clickable {
    cursor: pointer;
    transition: background-color 0.15s ease;
    border-radius: 6px;
    margin: 0 -8px;
    padding-left: 8px;
    padding-right: 8px;
}

.tx-similar-row.is-clickable:hover,
.tx-similar-row.is-clickable:focus {background-color: var(--prism-tint-2);
    outline: none;
}

.tx-similar-name {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;color: var(--prism-text);
}

.tx-similar-name-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Tiny pink dot to the LEFT of the name when a related-activity row hasn't
   been reviewed yet. Greens out (transparent border) when reviewed so the
   name still left-aligns identically across reviewed and unreviewed rows. */
.tx-similar-review-dot {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #fd5d93;
    box-shadow: 0 0 0 2px rgba(253, 93, 147, 0.18);
}
.tx-similar-review-dot.is-reviewed {
    background: #2dce89;
    box-shadow: 0 0 0 2px rgba(45, 206, 137, 0.18);
}

/* Inline "Pending" chip after the name. Same amber palette as
   .prism-badge-pending but compact (no outer padding) so it fits inside the
   single-line .tx-similar-row grid track. */
.tx-similar-pending-chip {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(245, 158, 11, 0.18);
    color: #f59e0b;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}
.tx-similar-pending-chip i {
    font-size: 0.85em;
}
.white-content .tx-similar-pending-chip {
    background: rgba(245, 158, 11, 0.14);
    color: #b45309;
}

.tx-similar-account {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--prism-fs-sm);
}

.tx-similar-cat {
    display: flex;
    justify-content: flex-end;
}

.tx-similar-amount {
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
}

.tx-similar-amount.positive { color: var(--prism-success); }
.tx-similar-amount.negative { color: var(--prism-danger); }

/* Clickable Prism-suggestion affordance shown when an uncategorized bank
   transaction has a Plaid-derived suggestion. The cell renders the same
   category pill the suggestion would apply, plus a subtle "Accept ✓" inline
   action. The "this is a suggestion" annotation lives in the label-level
   [SUGGESTED] chip, so this row stays uncluttered. */
.tx-bank-suggestion {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    padding: 4px 6px 4px 4px;
    border-radius: 999px;
    transition: background-color 0.15s ease, transform 0.15s ease;
}
.tx-bank-suggestion:hover,
.tx-bank-suggestion:focus {
    background: rgba(155, 89, 182, 0.10);
    outline: none;
}
.tx-bank-suggestion:hover .cat-badge,
.tx-bank-suggestion:focus .cat-badge {
    transform: translateY(-1px);
}
.tx-bank-suggestion__accept {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    letter-spacing: 0.3px;
    text-transform: uppercase;
    color: #9b59b6;
    opacity: 0.9;
    background: rgba(155, 89, 182, 0.08);
    border: 1px solid rgba(155, 89, 182, 0.35);
    border-radius: 999px;
    cursor: pointer;
    padding: 4px 12px;
    transition:
        background-color 0.18s ease,
        border-color 0.18s ease,
        color 0.18s ease,
        box-shadow 0.18s ease,
        transform 0.18s ease,
        opacity 0.18s ease;
    animation: tx-bank-suggestion-pulse 2.4s ease-in-out infinite;
}
.tx-bank-suggestion__accept:hover,
.tx-bank-suggestion__accept:focus {
    opacity: 1;
    outline: none;
    color: #fff;
    background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
    border-color: #8e44ad;
    box-shadow: 0 4px 14px rgba(155, 89, 182, 0.45);
    transform: translateY(-1px) scale(1.04);
    animation: none;
}
.tx-bank-suggestion__accept:active {
    transform: translateY(0) scale(1);
    box-shadow: 0 2px 6px rgba(155, 89, 182, 0.35);
}
.tx-bank-suggestion__accept:hover i.tim-icons,
.tx-bank-suggestion__accept:focus i.tim-icons {
    transform: translateX(1px) scale(1.12);
}
.tx-bank-suggestion__accept i.tim-icons {
    font-size: 0.95em;
    transition: transform 0.18s ease;
}

@keyframes tx-bank-suggestion-pulse {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(155, 89, 182, 0.35);
    }
    50% {
        box-shadow: 0 0 0 6px rgba(155, 89, 182, 0);
    }
}

.tx-bank-suggestion:hover .tx-bank-suggestion__accept,
.tx-bank-suggestion:focus .tx-bank-suggestion__accept {
    opacity: 1;
}

/* Hybrid cell: editable category picker + inline "Accept" affordance for
   the bank suggestion. The wrapper inherits .tx-cat-editable for the
   absolute-positioned dropdown menu, and switches to inline-flex so the
   trigger pill and Accept button sit on the same line. */
.tx-bank-suggestion-cell {
    display: inline-flex !important;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}
.white-content .tx-bank-suggestion:hover,
.white-content .tx-bank-suggestion:focus {
    background: rgba(155, 89, 182, 0.08);
}

.white-content .tx-bank-suggestion__accept {
    background: rgba(155, 89, 182, 0.06);
    border-color: rgba(155, 89, 182, 0.4);
    color: #8e44ad;
}
.white-content .tx-bank-suggestion__accept:hover,
.white-content .tx-bank-suggestion__accept:focus {
    box-shadow: 0 4px 14px rgba(155, 89, 182, 0.35);
}

/* Compact action row for the transaction-detail footer — keeps all buttons on a single line */
.tx-detail-actions__cluster {
    display: flex;
    gap: 6px;
    flex-wrap: nowrap;
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
}

.tx-detail-actions .prism-btn-sm {
    padding: 6px 10px;
    font-size: var(--prism-fs-2xs);
    letter-spacing: 0.3px;
    white-space: nowrap;
    flex: 0 0 auto;
}

.tx-detail-actions .prism-btn-sm i.tim-icons {
    font-size: 0.85em;
    margin-right: 3px;
}

/* Light-mode parity */

/* Custom Select Dropdown Styles */
.dropdown-field {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 16px;
}

.dropdown-label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.85em;color: var(--prism-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: 600;
}

.dropdown-label i {
    font-size: 1.1em;
    color: var(--prism-accent-text);
}

.optional-text {
    font-size: 0.9em;color: var(--prism-text-faint);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
}

.custom-select-wrapper {
    position: relative;
}

.custom-select {
    position: relative;
    width: 100%;
    user-select: none;
}

.select-trigger {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.08) 0%, rgba(var(--prism-accent-rgb), 0.03) 100%);border: 2px solid var(--prism-accent-border);
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.3s ease;
    min-height: 56px;
}

.select-trigger:hover {background: linear-gradient(135deg, var(--prism-tint-4) 0%, var(--prism-tint-1) 100%);
    border-color: rgba(var(--prism-accent-rgb), 0.4);
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(var(--prism-accent-rgb), 0.2);
}

.custom-select.open .select-trigger {
    border-color: var(--prism-accent);
    box-shadow: 0 0 0 3px rgba(var(--prism-accent-rgb), 0.2);
}

.select-emoji {
    font-size: 1.8em;
    line-height: 1;
    flex-shrink: 0;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}

.select-text {
    flex: 1;
    font-size: 0.95em;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.9);
    line-height: 1.3;
}

.select-trigger:hover .select-text {color: var(--prism-text);
}

.select-arrow {
    flex-shrink: 0;
    font-size: 0.9em;color: var(--prism-text-dim);
    transition: transform 0.3s ease;
}

.custom-select.open .select-arrow {
    transform: rotate(180deg);
    color: var(--prism-accent-text);
}

.select-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    background: var(--prism-overlay);
    backdrop-filter: blur(20px);border: 2px solid var(--prism-accent-border);
    border-radius: 12px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
    max-height: 320px;
    overflow-y: auto;
    z-index: 1000;
    opacity: 0;
    transform: translateY(-10px);
    pointer-events: none;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

    overscroll-behavior: contain;}

.custom-select.open .select-dropdown {
    opacity: 1;
    transform: translateY(0);
    pointer-events: all;
}

.select-dropdown::-webkit-scrollbar {
    width: 8px;
}

.select-dropdown::-webkit-scrollbar-track {background: var(--prism-surface-2);
    border-radius: 4px;
}

.select-dropdown::-webkit-scrollbar-thumb {background: var(--prism-fill-2);
    border-radius: 4px;
}

.select-dropdown::-webkit-scrollbar-thumb:hover {background: var(--prism-fill-2);
}

.select-group-label {
    padding: 10px 16px;
    font-size: 0.75em;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1px;color: var(--prism-text-dim);background: var(--prism-tint-3);border-bottom: 1px solid var(--prism-accent-border);
    position: sticky;
    top: 0;
    z-index: 1;
}

.select-option {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    cursor: pointer;
    transition: all 0.2s ease;
    border-left: 3px solid transparent;
}

.select-option:hover {background: linear-gradient(90deg, var(--prism-tint-4) 0%, var(--prism-tint-1) 100%);
    border-left-color: var(--prism-accent);
}

/* The stripe is the tick's colour, so it reads the tick's token. It was
   #00f2c3 here and #00bf9a in the light rule below, while ::after has always
   used var(--prism-success) -- identical in dark, two different greens in
   light. */
.select-option.selected {
    background: linear-gradient(90deg, rgba(var(--prism-accent-rgb), 0.25) 0%, rgba(var(--prism-accent-rgb), 0.1) 100%);
    border-left-color: var(--prism-success);
}

.select-option.selected::after {
    content: '\2713';
    margin-left: auto;color: var(--prism-success);
    font-weight: bold;
    font-size: 1.2em;
}

.option-emoji {
    font-size: 1.5em;
    line-height: 1;
    flex-shrink: 0;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}

.option-text {
    flex: 1;
    font-size: 0.9em;
    font-weight: 600;color: var(--prism-text-2);
}

.select-option:hover .option-text {color: var(--prism-text);
}

.select-option.selected .option-text {color: var(--prism-accent-text);
    font-weight: 700;
}

/* Category Command Palette */
.category-palette {
    display: flex;
    flex-direction: column;
    gap: 10px;
    flex: 1;
    min-height: 0;
}

.palette-search-bar {
    position: relative;
    display: flex;
    align-items: center;background: var(--prism-tint-2);border: 2px solid var(--prism-accent-border);
    border-radius: 12px;
    padding: 0 12px;
    transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
}

.palette-search-bar:focus-within {
    border-color: var(--prism-accent);background: var(--prism-tint-3);
    box-shadow: 0 0 0 3px rgba(var(--prism-accent-rgb), 0.15);
}

.palette-search-icon {
    color: rgba(255, 255, 255, 0.5);
    font-size: 1.05em;
    margin-right: 10px;
    flex-shrink: 0;
}

.palette-search-input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;color: var(--prism-text);
    padding: 12px 0;
    font-size: 0.95em;
    font-weight: 500;
}

.palette-search-input::placeholder {color: var(--prism-text-faint);
}

.palette-search-clear {
    background: none;
    border: none;
    color: rgba(255, 255, 255, 0.4);
    cursor: pointer;
    padding: 4px 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: color 0.12s ease;
}

.palette-search-clear:hover {
    color: var(--prism-danger);
}

.palette-list {
    flex: 1;
    min-height: 200px;
    max-height: 420px;
    overflow-y: auto;
    background: rgba(255, 255, 255, 0.02);border: 1px solid var(--prism-border-faint);
    border-radius: 12px;
    padding: 4px 0;

    overscroll-behavior: contain;}

.palette-list::-webkit-scrollbar {
    width: 6px;
}

.palette-list::-webkit-scrollbar-track {
    background: transparent;
}

.palette-list::-webkit-scrollbar-thumb {
    background: rgba(var(--prism-accent-rgb), 0.3);
    border-radius: 3px;
}

.palette-list::-webkit-scrollbar-thumb:hover {
    background: rgba(var(--prism-accent-rgb), 0.5);
}

.palette-group-header {
    padding: 10px 14px 6px;
    font-size: 0.7em;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1px;color: var(--prism-text-faint);
    position: sticky;
    top: 0;
    background: rgba(18, 27, 47, 0.95);
    backdrop-filter: blur(8px);
    z-index: 1;
}

.palette-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 14px;
    margin: 2px 6px;
    border-radius: 8px;
    cursor: pointer;
    border-left: 3px solid transparent;
    background: transparent;
    transition: background 0.08s ease, transform 0.08s ease, border-left-width 0.08s ease;
}

.palette-row.focused {background: var(--prism-tint-4);
    border-left-width: 4px;
}

.palette-row.selected {
    background: rgba(0, 242, 195, 0.1);
    border-left-width: 4px;
}

.palette-row.selected.focused {
    background: rgba(0, 242, 195, 0.18);
}

.palette-row-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.35em;
    line-height: 1;
}

.palette-row-icon img {
    width: 24px;
    height: 24px;
    object-fit: contain;
}

.palette-row-text {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;color: var(--prism-text-muted);
    font-size: 0.93em;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.palette-row-parent {color: var(--prism-text-dim);
    font-weight: 500;
}

.palette-row-arrow {color: var(--prism-text-faint);
    font-size: 0.9em;
}

.palette-row-sub {color: var(--prism-text);
    font-weight: 600;
}

.palette-row-check {
    color: var(--prism-success);
    font-size: 1em;
    flex-shrink: 0;
}

.palette-empty {
    padding: 30px 14px;
    text-align: center;color: var(--prism-text-faint);
    font-size: 0.9em;
    font-style: italic;
}

/* Category Tabs (legacy - kept for backward compatibility) */
.category-tabs {
    display: flex;
    gap: 8px;background: var(--prism-surface-2);
    border-radius: 12px;
    padding: 6px;border: 1px solid var(--prism-border-faint);
}

.category-tab {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: transparent;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.3s ease;color: var(--prism-text-dim);
    font-weight: 600;
    font-size: 0.9em;
    position: relative;
    overflow: hidden;
}

.category-tab::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.1) 0%, transparent 100%);
    opacity: 0;
    transition: opacity 0.3s ease;
}

.category-tab:hover::before {
    opacity: 1;
}

.category-tab:hover {color: var(--prism-text-muted);background: var(--prism-surface-2);
}

.category-tab.active {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.25) 0%, rgba(var(--prism-accent-rgb), 0.15) 100%);
    color: #fff;
    box-shadow: 0 4px 12px rgba(var(--prism-accent-rgb), 0.3);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.4);
}

.tab-icon {
    font-size: 1.2em;
    line-height: 1;
}

.tab-label {
    font-weight: 600;
}

.tab-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    background: rgba(var(--prism-accent-rgb), 0.2);
    border-radius: 10px;
    font-size: 0.75em;
    font-weight: 700;
}

.category-tab.active .tab-count {
    background: rgba(var(--prism-accent-rgb), 0.4);
    color: #fff;
}

/* Category Content Wrapper */
.category-content-wrapper {
    flex: 1;
    overflow-y: auto;
    position: relative;

    overscroll-behavior: contain;}

/* Category Tab Content */
.category-tab-content {
    display: none;
    animation: fadeIn 0.3s ease;
}

.category-tab-content.active {
    display: block;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Subcategory Container */
.category-subcategory-container {
    margin-top: 20px;
    padding-top: 20px;
    border-top: 2px solid rgba(255, 255, 255, 0.1);
    animation: slideDown 0.3s ease;
}

@keyframes slideDown {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.subcategory-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
}

.subcategory-title {
    font-size: 0.95em;
    font-weight: 600;color: var(--prism-text-2);
    display: flex;
    align-items: center;
    gap: 8px;
}

.subcategory-title i {
    color: var(--prism-accent-text);
}

.back-button {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;background: var(--prism-surface-2);border: 1px solid var(--prism-border-strong);
    border-radius: 8px;color: var(--prism-text-dim);
    font-size: 0.85em;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
}

.back-button:hover {background: var(--prism-surface-hover);
    border-color: rgba(var(--prism-accent-rgb), 0.4);
    color: var(--prism-accent-text);
    transform: translateX(-2px);
}

.back-button i {
    font-size: 0.9em;
}

/* Footer Actions. `.category-footer-actions` was kept here as an alias for the
   three category renderers; they are one renderer now and it uses the canonical
   name, so the alias had no callers left. */
.sidebar-footer-actions {
    flex-shrink: 0;
    margin-top: 20px;
    padding-top: 20px;border-top: 2px solid var(--prism-border);
    display: flex;
    gap: 12px;
    justify-content: space-between;
    align-items: center;
}

/* White Content Mode Overrides */

.white-content .palette-search-icon,
.white-content .palette-search-clear {
    color: rgba(0, 0, 0, 0.4);
}

.white-content .palette-list {
    background: rgba(var(--prism-accent-rgb), 0.03);
}

.white-content .palette-group-header {
    background: rgba(248, 249, 250, 0.98);
}

.white-content .palette-row.selected {
    background: rgba(0, 188, 140, 0.1);
}

.white-content .palette-row.selected.focused {
    background: rgba(0, 188, 140, 0.16);
}

/* White Content (Light Mode) - Custom Select Overrides */

.white-content .select-trigger {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.08) 0%, rgba(var(--prism-accent-rgb), 0.02) 100%);
}

.white-content .select-text {
    color: #525f7f;
}

.white-content .select-option {
    border-left-color: transparent;
}

/* Only the wash alphas survive here: the stripe is a token now, so restating
   it would be the shape the ratchet rejects. */
.white-content .select-option.selected {
    background: linear-gradient(90deg, rgba(var(--prism-accent-rgb), 0.15) 0%, rgba(var(--prism-accent-rgb), 0.06) 100%);
}

.white-content .category-tab.active {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.15) 0%, rgba(var(--prism-accent-rgb), 0.08) 100%);
    color: var(--prism-accent-text);
}

/* Eight .white-content .category-card* rules used to sit here, restating
   rgba(226,227,228,.6), #525f7f, #1d253b and #fff. They were on the list for
   this commit as overrides to retire by tokenizing their base rule. There is
   no base rule: `.category-card`, `-name`, `-emoji`, `-check` and
   `.category-cards-grid` appear in no template and no script — prism-common.css
   is the only file in the repo that mentions them at all. So they were not
   overriding anything, they were styling a class that does not exist, and
   eight of the light-mode overrides this file counts against itself were pure
   accounting. Deleted rather than converted.

   (`.sidebar-category-card` read as the same family under a different name and
   was left alone at the time. It turned out to be dead too — a 120-line grid of
   selectable cards, still on pre-token literals, that no template or script had
   ever used. It has since gone the same way, and `.prism-choice-card` is the
   live component it was reaching for.) */

/* Responsive Adjustments */
@media (max-width: 768px) {
    .transaction-header-content {
        flex-direction: column;
        align-items: flex-start;
    }

    .transaction-amount {
        text-align: left;
        margin-top: 8px;
    }

    .category-tabs {
        flex-direction: column;
    }

    .category-tab {
        justify-content: flex-start;
        padding: 10px 12px;
    }
}

@media (max-width: 480px) {
    .subcategory-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 12px;
    }
}

/* ── Category Badge Pills ── */
.cat-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 8px;
    border-radius: 999px;
    border: 1.5px solid;
    font-size: 0.80rem;
    font-weight: 500;
    white-space: nowrap;
    line-height: 1.4;
}
.cat-badge img {
    width: 13px;
    height: 13px;
    vertical-align: middle;
}
.category-badges-wrap {
    display: flex;
    align-items: center;
    gap: 5px;
    flex-wrap: wrap;
}
.category-badges-wrap .cat-sep {
    color: var(--prism-text-muted);
    font-size: 0.75rem;
}

/* ── Status Pills (shared with category-badge aesthetic) ── */
.status-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 8px;
    border-radius: 999px;
    border: 1.5px solid;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    margin-left: 4px;
}
.status-pill-pending {
    border-color: #ffc107;
    color: #ffc107;
    background-color: rgba(255, 193, 7, 0.12);
}
.status-pill-cash {
    border-color: #00bf9a;
    color: var(--prism-success);
    background-color: rgba(0, 191, 154, 0.12);
}
.status-pill-bank {
    border-color: var(--prism-accent);
    color: var(--prism-accent-text);
    background-color: rgba(var(--prism-accent-rgb), 0.12);
}
.status-pill-split {
    border-color: var(--prism-accent3);
    color: var(--prism-accent3);
    background-color: rgba(var(--prism-accent3-rgb), 0.12);
}

/* ===== GLOBAL SEARCH MODAL STYLES ===== */
/* Used in: navigation.html, navigation.js */

.search-modal-dialog {
    margin: 80px auto;
}

.search-modal-content {
    background: var(--prism-panel);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.35);
    border-radius: 16px;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(var(--prism-accent-rgb), 0.08);
    overflow: hidden;
}

/* Search input bar */
.search-modal-bar {
    display: flex;
    align-items: center;
    padding: 18px 22px;
    border-bottom: 1px solid rgba(var(--prism-accent-rgb), 0.15);
    gap: 14px;
}

.search-modal-icon {
    color: var(--prism-accent-text);
    font-size: 1.2em;
    flex-shrink: 0;
    opacity: 0.7;
}

.search-modal-input {
    flex: 1;
    background: transparent !important;
    border: none !important;
    outline: none !important;
    box-shadow: none !important;color: var(--prism-text);
    font-size: 1.1rem;
    font-weight: 400;
    padding: 0;
    line-height: 1.5;
}

.search-modal-input::placeholder {color: var(--prism-text-faint);
}

.search-modal-close-btn {
    flex-shrink: 0;
    background: transparent;border: 1px solid var(--prism-border);
    border-radius: 6px;
    cursor: pointer;
    padding: 4px 8px;
    line-height: 1;
    transition: all 0.2s ease;
}

.search-modal-close-btn:hover {background: var(--prism-surface-hover);border-color: var(--prism-border-strong);
}

.search-modal-close-btn kbd {
    font-size: 0.72rem;color: var(--prism-text-faint);
    font-family: inherit;
    background: none;
    border: none;
    padding: 0;
}

/* Results scroll container */
.search-modal-results {
    max-height: 440px;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 8px 0;

    overscroll-behavior: contain;}

/* Empty / hint state */
.search-modal-empty {
    padding: 28px 22px;
    display: flex;
    justify-content: center;
}

.search-modal-hint {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
}

.search-modal-hint span {
    display: flex;
    align-items: center;
    gap: 5px;color: var(--prism-text-faint);
    font-size: 0.8rem;
}

.search-modal-hint kbd {background: var(--prism-surface-hover);border: 1px solid var(--prism-border);
    border-radius: 4px;
    padding: 2px 5px;
    font-size: 0.7rem;color: var(--prism-text-faint);
    font-family: inherit;
}

/* Type group header */
.search-group-header {
    padding: 8px 22px 4px;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 1.5px;
    text-transform: uppercase;color: var(--prism-text-faint);border-top: 1px solid var(--prism-border-faint);
    margin-top: 4px;
}

.search-group-header:first-child {
    border-top: none;
    margin-top: 0;
}

/* Result row */
.search-result-item {
    display: flex !important;
    align-items: center;
    gap: 14px;
    padding: 10px 22px;
    cursor: pointer;
    transition: background 0.12s ease;
    text-decoration: none !important;
    color: inherit !important;
}

.search-result-item:hover {background: var(--prism-tint-4);
    text-decoration: none !important;
}

.search-result-item.selected {background: var(--prism-tint-4);
    border-left: 3px solid var(--prism-accent);
    padding-left: 19px; /* compensate for 3px border */
    text-decoration: none !important;
}

/* Type icon pill */
.search-result-icon {
    width: 34px;
    height: 34px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 0.95em;
}

.search-result-icon.type-property  { background: rgba(var(--prism-accent-rgb), 0.14); color: var(--prism-accent-text); }
.search-result-icon.type-account   { background: rgba(0, 242, 195, 0.12);color: var(--prism-success); }
.search-result-icon.type-user      { background: rgba(var(--prism-accent2-rgb), 0.12); color: var(--prism-accent2-text); }
.search-result-icon.type-transaction { background: rgba(255, 141, 114, 0.12); color: var(--prism-warning); }
.search-result-icon.type-default   {background: var(--prism-surface-hover);color: var(--prism-text-faint); }

/* Title & description */
.search-result-body {
    flex: 1;
    min-width: 0;
}

.search-result-title {
    font-weight: 600;
    font-size: 0.92rem;color: var(--prism-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin: 0 0 2px;
    line-height: 1.35;
}

.search-result-desc {
    font-size: 0.78rem;color: var(--prism-text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin: 0;
    line-height: 1.3;
}

/* Type badge (right side) */
.search-result-badge {
    flex-shrink: 0;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.6px;
    padding: 3px 8px;
    border-radius: 10px;
    text-transform: uppercase;
}

.search-result-badge.type-property    { background: rgba(var(--prism-accent-rgb), 0.14);  color: var(--prism-accent-text); }
.search-result-badge.type-account     {background: var(--prism-success-wash);color: var(--prism-success); }
.search-result-badge.type-user        { background: rgba(var(--prism-accent2-rgb), 0.12);  color: var(--prism-accent2-text); }
.search-result-badge.type-transaction { background: rgba(255, 141, 114, 0.12); color: var(--prism-warning); }
.search-result-badge.type-default     {background: var(--prism-surface-hover);color: var(--prism-text-faint); }

/* No results state */
.search-no-results {
    padding: 36px 22px;
    text-align: center;color: var(--prism-text-faint);
}

.search-no-results i {
    font-size: 1.8em;
    display: block;
    margin-bottom: 10px;
    opacity: 0.5;
}

.search-no-results p {
    font-size: 0.88rem;
    margin: 0;
}

/* ===== IMPERSONATION BANNER ===== */
/* Used in: footer.html */
@keyframes prism-impersonate-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(255, 141, 114, 0.4), inset 0 0 20px rgba(255, 141, 114, 0.03); }
    50%       { box-shadow: 0 0 0 6px rgba(255, 141, 114, 0), inset 0 0 20px rgba(255, 141, 114, 0.08); }
}

.prism-impersonation-banner {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 7px 14px 7px 12px;
    background: linear-gradient(90deg, rgba(255, 141, 114, 0.12) 0%, rgba(255, 141, 114, 0.05) 100%);border: 1px solid var(--prism-warning-border);
    border-left: 3px solid #ff8d72;
    border-radius: 8px;
    animation: prism-impersonate-pulse 2.5s ease-in-out infinite;
    font-size: 0.82rem;color: var(--prism-text-2);
    letter-spacing: 0.01em;
    vertical-align: middle;
}

.prism-impersonation-banner .prism-impersonate-icon {color: var(--prism-warning);
    font-size: 0.95rem;
    flex-shrink: 0;
}

.prism-impersonation-banner .prism-impersonate-label {color: var(--prism-text-muted);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 600;
}

.prism-impersonation-banner .prism-impersonate-user {color: var(--prism-warning);
    font-weight: 700;
    font-size: 0.88rem;
}

.prism-impersonation-banner .prism-btn-stop {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 11px;background: var(--prism-warning-wash);
    border: 1px solid rgba(255, 141, 114, 0.4);
    border-radius: 5px;color: var(--prism-warning);
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    text-decoration: none;
    transition: all 0.2s ease;
    flex-shrink: 0;
}

.prism-impersonation-banner .prism-btn-stop:hover {background: var(--prism-warning-wash);
    border-color: #ff8d72;color: var(--prism-warning);
    text-decoration: none;
    transform: translateY(-1px);
}

/* Light mode overrides */
.white-content .prism-impersonation-banner {
    background: linear-gradient(90deg, rgba(255, 141, 114, 0.1) 0%, rgba(255, 141, 114, 0.03) 100%);
    border-left-color: #e07050;
}

.white-content .prism-impersonation-banner .prism-btn-stop {
    border-color: rgba(200, 80, 48, 0.4);
}

.white-content .prism-impersonation-banner .prism-btn-stop:hover {
    border-color: #c05030;
}

/* ===== ASSET TYPE BADGES ===== */
/* Used in: page-user-v2.html, right_sidebar.js */
.asset-type-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 11px;
    border-radius: 20px;
    font-size: 0.82em;
    font-weight: 600;
    white-space: nowrap;
    border: 1px solid transparent;
}

.asset-type-residential {background: var(--prism-tint-3);
    color: var(--prism-accent-text);border-color: var(--prism-accent-border);
}

.asset-type-commercial {background: var(--prism-warning-wash);
    color: var(--prism-warning);border-color: var(--prism-warning-border);
}

.asset-type-business {background: var(--prism-accent2-wash);
    color: var(--prism-accent3);
    border-color: rgba(var(--prism-accent3-rgb), 0.3);
}

.white-content .asset-type-residential {
    color: #1060c0;
}

.white-content .asset-type-commercial {
    color: #b84800;
}

.white-content .asset-type-business {
    color: #7020b8;
    border-color: rgba(var(--prism-accent3-rgb), 0.25);
}

/* ===== SCOPE FILTER DROPDOWN ===== */
/* Used in: index.html, reports.html */

/* Trigger button — pill style matching prism-btn-secondary prism-btn-sm */
/* Neutral at rest. This was a 2px accent2 outline with accent2 text — the
   loudest control on the card, for a utility that opens a settings popover.
   It sat beside SHOW ALL / HIDE ALL and above the chart itself, competing with
   both, and on a skin whose accent2 differs sharply from its accent it read as
   a third colour that belonged to nothing on screen.

   Same principle the cards moved to: a surface at rest is neutral, and colour
   is spent on state. The state here is already carried — the count badge says
   how many filters are active — so the accent comes back on hover and when the
   popover is open, and stays out of the way otherwise. */
.ncf-filter-btn {
    background: var(--prism-veil-2);
    border: 1px solid var(--prism-border);
    color: var(--prism-text-2);
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 1px;
    text-transform: uppercase;
    border-radius: var(--prism-radius-pill);
    padding: 8px 20px;
    transition: all 0.4s ease;
    position: relative;
    cursor: pointer;
    line-height: 1;
}
.ncf-filter-btn:hover, .ncf-filter-btn:focus,
.ncf-filter-btn[aria-expanded="true"] {
    background: var(--prism-tint-3);
    border-color: var(--prism-accent-border);
    transform: translateY(-2px);
    color: var(--prism-text);
    box-shadow: 0 8px 25px rgba(var(--prism-accent-rgb), 0.18);
    border-color: var(--prism-accent3);
    outline: none;
}
/* Filters are on. This is the one state that earns the accent outright, and it
   needs to out-read the hover above it, so it takes a fill as well as the
   border rather than relying on a 1px edge. */
.ncf-filter-btn--active {
    background: var(--prism-tint-3);
    border-color: var(--prism-accent);
    color: var(--prism-accent-text);
}
.ncf-filter-btn--active:hover, .ncf-filter-btn--active:focus {
    background: var(--prism-tint-4);
    border-color: var(--prism-accent-light);
    color: var(--prism-text);
    box-shadow: 0 8px 25px rgba(var(--prism-accent-rgb), 0.25);
}

.white-content .ncf-filter-btn:hover {
    background: rgba(var(--prism-accent2-rgb), 0.1);
    border-color: var(--prism-accent3);
    color: var(--prism-accent2-text);
}

/* Count pill */
.ncf-filter-count {
    display: inline-block;
    background: rgba(var(--prism-accent2-rgb), 0.2);
    color: var(--prism-accent2-text);
    font-size: 0.65rem;
    font-weight: 700;
    min-width: 18px;
    height: 16px;
    line-height: 16px;
    border-radius: 8px;
    text-align: center;
    padding: 0 4px;
    margin-left: 5px;
    vertical-align: middle;
    border: 1px solid rgba(var(--prism-accent2-rgb), 0.35);
}

/* Panel */
.ncf-filter-dropdown {
    min-width: 272px;
    padding: 0;
    border-radius: 10px;border: 1px solid var(--prism-accent-border);
    background: linear-gradient(170deg, rgba(var(--prism-accent-rgb), 0.12) 0%, rgba(14, 15, 30, 0.98) 40%);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(var(--prism-accent-rgb), 0.18);
    overflow: hidden;
    margin-top: 6px !important;
}
.ncf-filter-dropdown::before {
    content: '';
    display: block;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.6) 50%, transparent 100%);
}
.white-content .ncf-filter-dropdown {
    background: linear-gradient(170deg, rgba(var(--prism-accent-rgb), 0.07) 0%, rgba(232, 234, 239, 0.99) 40%);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(var(--prism-accent-rgb), 0.12);
}

/* Header */
.ncf-filter-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 11px 16px 9px;border-bottom: 1px solid var(--prism-accent-border);
}
.ncf-filter-label {
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--prism-accent-text);
}
.ncf-filter-actions {
    display: flex;
    gap: 5px;
}
.ncf-filter-actions a {
    display: inline-block;
    padding: 2px 8px;border: 1px solid var(--prism-border);
    border-radius: 10px;color: var(--prism-text-dim);
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s;
}
.ncf-filter-actions a:hover {
    border-color: rgba(var(--prism-accent-rgb), 0.6);
    color: var(--prism-accent-text);
    text-decoration: none;
}

.white-content .ncf-filter-actions a:hover {
    border-color: var(--prism-accent);
}

/* List */
.ncf-filter-list {
    max-height: 232px;
    overflow-y: auto;
    padding: 4px 0;

    overscroll-behavior: contain;}

/* Row */
.ncf-filter-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 16px;
    height: 34px;
    margin: 0;
    cursor: pointer;
    transition: background 0.12s ease;
    font-weight: 400;
    user-select: none;
}
.ncf-filter-item:hover {background: var(--prism-tint-2);
}

.ncf-filter-item--highlight {background: var(--prism-tint-4);
}

/* Hide native checkbox */
.ncf-filter-item input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

/* Pill indicator */
.ncf-filter-pill {
    flex-shrink: 0;
    width: 22px;
    height: 10px;
    border-radius: 5px;border: 1.5px solid var(--prism-border-strong);
    background: transparent;
    transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

.ncf-filter-item input:checked ~ .ncf-filter-pill {
    background: var(--prism-accent);
    border-color: var(--prism-accent);
    box-shadow: 0 0 8px rgba(var(--prism-accent-rgb), 0.45);
}

/* Asset name */
.ncf-filter-name {
    font-size: 0.8rem;color: var(--prism-text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
    transition: color 0.18s ease, opacity 0.18s ease;
}
.ncf-filter-item input:checked ~ .ncf-filter-name {color: var(--prism-text);
}

/* Search input */
.ncf-filter-search-wrap {
    padding: 8px 12px;border-bottom: 1px solid var(--prism-accent-border);
}
.ncf-filter-search {
    width: 100%;
    background: rgba(255, 255, 255, 0.05);border: 1px solid var(--prism-accent-border);
    border-radius: 5px;
    padding: 5px 10px 5px 28px;
    font-size: 0.76rem;color: var(--prism-text-muted);
    outline: none;
    transition: border-color 0.15s ease, background 0.15s ease;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.3)' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 9px center;
}
.ncf-filter-search::placeholder {color: var(--prism-text-faint);
}
.ncf-filter-search:focus {border-color: var(--prism-accent-border);
    background-color: rgba(255, 255, 255, 0.08);
}

.white-content .ncf-filter-search {
    background-color: rgba(29, 37, 59, 0.04);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='rgba(29,37,59,0.3)' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
}

.white-content .ncf-filter-search:focus {
    background-color: rgba(var(--prism-accent-rgb), 0.04);
}
.ncf-filter-no-results {
    padding: 12px 16px;
    font-size: 0.76rem;color: var(--prism-text-faint);
    text-align: center;
    display: none;
}

/* Footer */
.ncf-filter-footer {
    padding: 9px 16px 12px;
    display: flex;
    justify-content: flex-end;
    position: relative;
}
.ncf-filter-footer::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.25) 50%, transparent 100%);
}

/* ===== CASHFLOW CHART OPTIONS PANEL ===== */
/* Used in: index.html — the single "Options" popover that replaced the twelve
   pills (range / partial month / cash / scope / omit tags / ownership) that used
   to wrap onto two rows in the Net Cash Flow card header.

   Only the shell is defined here. The trigger is .ncf-filter-btn, the asset
   section reuses the .ncf-filter-* header/search/list/item classes above, the tag
   rows reuse .chart-tag-omit-row, and the two segments reuse
   .prism-ownership-toggle — so all of it was already proven across the nine skins.
   Colours here are tokens end to end, which is why there is no .white-content
   block: light mode is a token flip, not a second stylesheet. */
.ncf-options-panel {
    width: 340px;
    max-width: calc(100vw - 32px);
    /* Tall enough that a company with a handful of assets and tags sees every
       section without scrolling. The inner lists are capped well below their
       content for the same reason: when the panel does scroll, it must cut
       through a list mid-row, because a section heading sitting flush against
       the footer with nothing under it reads as an empty section rather than a
       scrollable one. */
    max-height: min(78vh, 640px);
    overflow-y: auto;
    padding: 0;
    text-align: left;
    background: var(--prism-surface-raised);
    border: 1px solid var(--prism-border);
    border-radius: 12px;
    /* Two stops of the same token rather than one literal rgba: --prism-shadow-soft
       is mode-aware, so light mode gets a lighter shadow for free instead of the
       heavy black one a floating layer needs over a dark ground. */
    box-shadow: 0 20px 60px var(--prism-shadow-soft), 0 4px 14px var(--prism-shadow-soft);
    margin-top: 6px !important;

    overscroll-behavior: contain;}

.ncf-options-section {
    padding: 12px 16px;
}
/* Separator goes on the *following* section rather than a trailing border, so the
   last one never doubles up with the footer's own border-top. :last-of-type would
   not have helped — the footer is the panel's last div, so it, not the last
   section, is what that selector matches. */
.ncf-options-section + .ncf-options-section {
    border-top: 1px solid var(--prism-border);
}
/* The asset list brings its own .ncf-filter-header / -search-wrap padding and
   wants to run edge to edge, so it opts out of the section's own padding. */
.ncf-options-section--flush {
    padding: 0;
}

.ncf-options-section-title {
    display: block;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--prism-accent-text);
    margin-bottom: 10px;
}

/* One switch per line: label left, control hard right. */
.ncf-options-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}
.ncf-options-row + .ncf-options-row {
    margin-top: 10px;
}
.ncf-options-row-label {
    font-size: 0.82rem;
    line-height: 1.3;
    color: var(--prism-text);
}

.ncf-options-note {
    margin: 9px 0 0;
    font-size: 0.7rem;
    line-height: 1.45;
    color: var(--prism-text-muted);
}

/* .prism-ownership-toggle pills are sized for a card header (8px 20px). Inside a
   340px panel four of them must wrap and shrink instead of overflowing. */
.ncf-options-section .prism-ownership-toggle {
    display: flex;
    flex-wrap: wrap;
}
.ncf-options-section .prism-ownership-toggle .btn {
    padding: 6px 15px;
    font-size: 0.75rem;
}

/* Both inner lists scroll on their own, so the panel's own scrollbar only ever
   moves between sections rather than fighting a nested one. */
.ncf-options-section .ncf-filter-list {
    max-height: 140px;
}
.ncf-options-tag-list {
    max-height: 132px;
    overflow-y: auto;

    overscroll-behavior: contain;}

/* .prism-card is overflow:hidden (for its rounded corners) and position:relative,
   so it both contains and clips this panel. Measured today the panel tops out
   132px short of the card's bottom edge, so nothing is actually cut off — but
   that margin is the only thing standing between a taller panel, or a shorter
   card, and a clipped Apply button, which would be unreachable rather than merely
   ugly. Lift the clip while the panel is open and the hazard is gone for good.
   z-index because a later sibling card would otherwise paint over what escapes. */
.prism-card.ncf-options-open {
    overflow: visible;
    z-index: 5;
}

.ncf-options-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 11px 16px;
    border-top: 1px solid var(--prism-border);
    position: sticky;
    bottom: 0;
    background: var(--prism-surface-raised);
    /* The footer floats over the panel's scroll, so it needs to cast a shadow
       upward — without one, content sliding under it just vanishes and the
       panel looks like it ends there. */
    box-shadow: 0 -8px 12px -6px var(--prism-shadow-soft);
}
.ncf-options-reset {
    background: none;
    border: none;
    padding: 0;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--prism-text-muted);
    cursor: pointer;
    transition: color 0.15s ease;
}
.ncf-options-reset:hover,
.ncf-options-reset:focus {
    color: var(--prism-accent-text);
    outline: none;
}
.ncf-options-footer .prism-btn-sm {
    padding: 6px 22px;
}

/* ===== DASHBOARD STRIP ===== */
/* Used in: index.html (dashboard-strip.js runs the search). One line above the
   Net Cash Flow chart: the welcome, the two balances and three icon controls.
   It replaced 404px of chrome — a centred banner, a Show Values / Clear Cache
   row, two h3 balance lines and a full-width search box — which is why the
   chart used to open below the fold on a laptop.

   The search is collapsed to its magnifier and grows leftward out of it when
   opened: width 0 -> 300px on .is-open. `visibility` is the accessibility
   half — a collapsed input must not take keyboard focus, and width alone
   leaves it in the tab order. Tokens end to end; no .white-content twin. */
/* The strip's height is set by the collapsed search wrapper (~38px), not by any
   of the type below it, so these sizes can grow a fair way before the row stops
   being one line — test_the_whole_strip_is_one_line is what says where that is. */
.dash-strip {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px 18px;
    margin-bottom: 18px;
}
.dash-strip-welcome {
    margin: 0;
    font-size: 1.35rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    white-space: nowrap;
    line-height: 1.2;
}
.dash-strip-stats {
    display: flex;
    align-items: center;
    gap: 18px;
    margin-left: auto;
    min-width: 0;
}
/* Label and figure on ONE baseline, not stacked: a stacked stat is two lines
   tall, so the strip that exists to be one line was the tallest thing on it.
   Same type as before — the kicker and the display figure are unchanged. */
.dash-stat {
    display: flex;
    flex-direction: row;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
    line-height: 1.2;
}
.dash-stat-k {
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--prism-text-muted);
}
.dash-stat-v {
    font-family: var(--prism-font-display);
    font-variant-numeric: tabular-nums;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--prism-text);
}
.dash-strip-search {
    position: relative;
    width: 0;
    opacity: 0;
    visibility: hidden;
    overflow: hidden;
    transition: width 0.2s ease, opacity 0.2s ease, visibility 0s linear 0.2s;
}
.dash-strip-search.is-open {
    width: 300px;
    opacity: 1;
    visibility: visible;
    overflow: visible;
    transition: width 0.2s ease, opacity 0.2s ease, visibility 0s;
}
/* An explicit transition list, not the shared rule's `transition: all`: `all`
   animates the inherited `visibility` too, so for the first frame after the
   wrapper opens the input is still computed hidden and refuses focus() — the
   caret never lands in the field the click just opened. Found by running it. */
.dash-strip-search .prism-search-input {
    margin: 0;
    padding: 8px 16px;
    font-size: 0.9rem;
    transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
}
.dash-strip-count {
    position: absolute;
    top: calc(100% + 4px);
    right: 4px;
    font-size: 0.72rem;
    white-space: nowrap;
    color: var(--prism-text-muted);
}
.dash-strip-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--prism-border);
    background: var(--prism-veil-2);
    color: var(--prism-text-2);
    font-size: 0.95rem;
    cursor: pointer;
    text-decoration: none;
    transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}
.dash-strip-icon:hover,
.dash-strip-icon:focus-visible,
.dash-strip-icon[aria-expanded="true"] {
    border-color: var(--prism-accent);
    color: var(--prism-accent-text);
    background: var(--prism-tint-2);
    outline: none;
    text-decoration: none;
}
.dash-strip-icon.is-danger { color: var(--prism-danger); }
.dash-strip-icon.is-danger:hover,
.dash-strip-icon.is-danger:focus-visible {
    border-color: var(--prism-danger);
    background: var(--prism-danger-wash);
    color: var(--prism-danger);
}
@media (max-width: 900px) {
    .dash-strip-welcome { width: 100%; white-space: normal; }
    .dash-strip-stats { width: 100%; margin-left: 0; flex-wrap: wrap; }
    .dash-strip-search.is-open { width: 100%; }
}

/* ===== DASHBOARD ASSET PERFORMANCE ===== */
/* Used in: index.html (dashboard-asset-performance.js draws the rows).
   The by-asset rows INSIDE the Net Cash Flow card: one row per asset on a
   shared dollar axis — a grey dot for last month, a coloured dot for this
   month, a line for the move — under a slim bar that holds the month control
   and a one-line summary. It began as a second card under the chart with its
   own subtitle, title, pill cluster and a three-tile strip; that header and
   those tiles were the clutter, so the rows moved into the chart's card, the
   tiles became the sentence and the sort moved onto the column labels.

   Deliberately NOT the categories ledger's `.led-*` vocabulary: no column
   grid, no pill bars, no sparkline. The owner asked for that ledger's
   information without a second copy of its look on the dashboard, and the
   dumbbell is the form that says "then -> now" with the least ink.

   Tokens end to end, so there is no .white-content block: the dot ring reads
   --prism-bg-base and flips with the mode, the direction colours are the
   semantic tokens the chart already uses for the same job, and the hover is
   the Unreviewed row's own tint. */
.aperf-section {
    margin: 22px -25px -25px;
    padding: 16px 25px 20px;
    border-top: 1px solid var(--prism-border-faint);
}
.aperf-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px 18px;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.aperf-bar-left {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}
.aperf-dd { position: relative; }
.aperf-kicker {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--prism-accent-text);
    white-space: nowrap;
}
/* The old footnote, as a tooltip on the kicker. */
.aperf-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 1px solid var(--prism-text-muted);
    color: var(--prism-text-muted);
    font-family: var(--prism-font-body);
    font-size: 0.6rem;
    font-weight: 700;
    font-style: italic;
    letter-spacing: 0;
    text-transform: none;
    cursor: help;
}
.aperf-info:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 1px;
}

/* The month control is text, not a pill: the card's header already spends its
   one pill on Options, and a second pill cluster is what the old card's header
   looked like. The label and the "vs" share one button so the whole phrase
   opens the menu. */
.aperf-month-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 6px;
    background: none;
    border: 0;
    border-radius: 6px;
    font-family: var(--prism-font-display);
    font-size: 0.98rem;
    font-weight: 700;
    color: var(--prism-text);
    white-space: nowrap;
    cursor: pointer;
}
.aperf-month-btn:hover,
.aperf-month-btn:focus-visible,
.aperf-month-btn[aria-expanded="true"] {
    background: var(--prism-tint-2);
    outline: none;
}
.aperf-vs {
    font-family: var(--prism-font-body);
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--prism-text-muted);
}
.aperf-chev {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    margin-left: 2px;
    color: var(--prism-text-muted);
}
.aperf-month-btn[aria-expanded="true"] .aperf-chev {
    transform: rotate(-135deg) translateY(-2px);
}
.aperf-step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0 0 2px;
    border-radius: 50%;
    border: 1px solid var(--prism-border);
    background: transparent;
    color: var(--prism-text-2);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease;
}
.aperf-step:hover,
.aperf-step:focus-visible {
    border-color: var(--prism-accent);
    color: var(--prism-accent-text);
    outline: none;
}
.aperf-step[disabled],
.aperf-step[disabled]:hover {
    opacity: 0.4;
    cursor: default;
    border-color: var(--prism-border);
    color: var(--prism-text-2);
}

/* The sneak peek as one sentence: n of m improved, the top mover, the biggest
   drop. It replaced three tiles that restated the top and bottom rows. */
.aperf-summary {
    margin: 0;
    min-width: 0;
    font-size: 0.8rem;
    line-height: 1.6;
    text-align: right;
    color: var(--prism-text-2);
}
.aperf-sum-name {
    font-weight: 700;
    color: var(--prism-text);
}
.aperf-summary .aperf-delta { vertical-align: 1px; }

/* The month menu takes the Options panel's shell (.ncf-options-panel) and
   adds the list. `hidden` needs its own rule because the shell sets
   `display`. It opens inside the chart's card, whose overflow the script
   lifts with .ncf-options-open on THAT card — the class the Options panel
   itself uses; the control sits at the left of the bar, so it opens right. */
.aperf-menu {
    position: absolute;
    left: 0;
    top: calc(100% + 6px);
    z-index: 1000;
    display: block;
    width: 300px;
    padding: 8px 0;
    margin-top: 0 !important;
}
.aperf-menu[hidden] { display: none; }
.aperf-menu-title {
    display: block;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--prism-accent-text);
    padding: 6px 16px 6px;
}
.aperf-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 7px 16px;
    background: none;
    border: 0;
    color: var(--prism-text);
    font-size: 0.82rem;
    text-align: left;
    cursor: pointer;
}
.aperf-menu-item:hover,
.aperf-menu-item:focus {
    background: var(--prism-surface-hover);
    outline: none;
}
.aperf-menu-item.is-selected { background: var(--prism-tint-3); }
.aperf-menu-check {
    width: 14px;
    flex-shrink: 0;
    color: var(--prism-accent-text);
    font-weight: 800;
}
.aperf-menu-label { flex: 1 1 auto; min-width: 0; }
.aperf-menu-value {
    font-variant-numeric: tabular-nums;
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--prism-text-2);
}
.aperf-menu-value.is-negative { color: var(--prism-danger); }
.aperf-menu-note {
    margin: 8px 16px 4px;
    font-size: 0.7rem;
    line-height: 1.45;
    color: var(--prism-text-muted);
}

/* Rows. One grid for the axis header and every row so the four columns line
   up: name | axis | this month | change. */
.aperf-grid {
    display: grid;
    grid-template-columns: 230px minmax(0, 1fr) 100px 96px;
    column-gap: 14px;
    align-items: center;
}
.aperf-axis-head {
    align-items: end;
    padding: 0 8px 8px;
    margin-bottom: 2px;
    border-bottom: 1px solid var(--prism-border);
}
.aperf-lbl {
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--prism-text-muted);
}
.aperf-lbl.r { text-align: right; justify-self: end; }
/* The column labels are the sort control, in prism-table's .prism-sort classes
   so the shared rules apply: a button reset to the label's own type. The
   Sort pill and its menu that used to sit in the old card's header are gone. */
.aperf-lbl.prism-sort {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--prism-text-muted);
}
.aperf-ticks {
    position: relative;
    height: 30px;
}
.aperf-tick-label {
    position: absolute;
    bottom: 0;
    transform: translateX(-50%);
    font-size: 0.6rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--prism-text-muted);
}
.aperf-legend {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 0.62rem;
    white-space: nowrap;
    color: var(--prism-text-muted);
}
.aperf-legend-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 5px;
    vertical-align: middle;
}
.aperf-legend-dot.is-prev { background: var(--prism-text-dim); }
.aperf-legend-dot.is-cur { background: var(--prism-text); }
/* The same dot in the row's tooltip, which is the SHARED .stats-perf-tip panel
   (see its own block above): that component's chrome was written against
   prism-highcharts.js's tooltip theme, so the rows and the chart they sit under
   hand out the same object rather than two that resemble each other. What it
   does not carry is the chart tooltip's coloured series dot, and this is it —
   the direction colours the row itself already uses. */
.aperf-legend-dot.is-up { background: var(--prism-success); }
.aperf-legend-dot.is-down { background: var(--prism-danger); }

/* NINE rows at a time, then it scrolls: the list sits inside the chart's card,
   so its height is the card's height, and a thirty-asset portfolio pushed the
   chart off the top of the screen while you read the bottom of it. Nine (342px)
   still leaves the chart on a laptop and covers most portfolios without a
   scroll; six was the first cut and was tighter than it needed to be. The cap is
   derived from the row height rather than written as a number, so the two
   cannot drift — a taller row still shows exactly nine.

   `overscroll-behavior: contain` is the other half: without it, reaching the
   end of the list hands the wheel to the page, so a flick meant for the rows
   scrolls the dashboard out from under them. */
.aperf-rows {
    --aperf-row-h: 38px;
    --aperf-rows-visible: 9;
    max-height: calc(var(--aperf-row-h) * var(--aperf-rows-visible));
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    padding: 0 8px;
}
.aperf-row {
    height: var(--aperf-row-h);
    margin: 0 -8px;
    padding: 0 8px;
    border-bottom: 1px solid var(--prism-border-faint);
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.aperf-row:last-child { border-bottom: none; }
.aperf-row:hover,
.aperf-row:focus-visible {
    background: var(--prism-tint-2);
    outline: none;
}
.aperf-name {
    display: flex;
    align-items: center;
    gap: 11px;
    min-width: 0;
}
/* The tile is tinted the way the balance cards below tint by asset type. */
.aperf-icon {
    width: 30px;
    height: 30px;
    border-radius: 8px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    background: var(--prism-tint-4);
}
.aperf-icon.is-commercial { background: var(--prism-warning-wash); }
.aperf-icon.is-business { background: var(--prism-accent2-wash); }
.aperf-asset {
    min-width: 0;
    font-size: 0.9rem;
    font-weight: 650;
    color: var(--prism-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.aperf-axis {
    position: relative;
    height: 100%;
    min-height: 36px;
}
.aperf-tick,
.aperf-zero {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--prism-border-faint);
}
.aperf-zero { background: var(--prism-border-strong); }
.aperf-link {
    position: absolute;
    top: 50%;
    height: 2px;
    margin-top: -1px;
    border-radius: 2px;
    background: var(--prism-text-dim);
    opacity: 0.85;
}
.aperf-link.is-up { background: var(--prism-success); }
.aperf-link.is-down { background: var(--prism-danger); }
.aperf-dot {
    position: absolute;
    top: 50%;
    border-radius: 50%;
    box-shadow: 0 0 0 2px var(--prism-bg-base);
}
.aperf-dot.is-prev {
    width: 8px;
    height: 8px;
    margin: -4px 0 0 -4px;
    background: var(--prism-text-dim);
}
.aperf-dot.is-cur {
    width: 10px;
    height: 10px;
    margin: -5px 0 0 -5px;
    background: var(--prism-text);
}
.aperf-dot.is-cur.is-up { background: var(--prism-success); }
.aperf-dot.is-cur.is-down { background: var(--prism-danger); }
.aperf-net {
    text-align: right;
    font-family: var(--prism-font-display);
    font-variant-numeric: tabular-nums;
    font-size: 0.92rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--prism-text);
}
.aperf-net.is-negative { color: var(--prism-danger); }
.aperf-change { text-align: right; }
.aperf-delta {
    display: inline-block;
    padding: 2px 7px;
    border-radius: var(--prism-radius-pill);
    font-size: 0.72rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.aperf-delta.is-up {
    color: var(--prism-success);
    background: var(--prism-success-wash);
}
.aperf-delta.is-down {
    color: var(--prism-danger);
    background: var(--prism-danger-wash);
}
.aperf-delta.is-flat {
    color: var(--prism-text-muted);
    background: var(--prism-surface-hover);
}
.aperf-empty {
    padding: 24px;
    text-align: center;
    font-size: 0.85rem;
    color: var(--prism-text-muted);
}

/* The month the rows describe, tinted on the chart above them. Highcharts
   writes the band's fill as a presentation attribute, which author CSS
   outranks, so the band follows skin and mode with no observer — and the
   colour could not be resolved in JS anyway: prismResolveColor lives in
   right_sidebar.js, which loads after this page's scripts have run. */
.highcharts-plot-band.aperf-chart-band {
    fill: var(--prism-accent);
    fill-opacity: 0.1;
}

/* Below 1080px the axis goes and the row keeps name / this month / change —
   the same "always kept" trio as the categories ledger's last breakpoint. */
@media (max-width: 1080px) {
    .aperf-grid { grid-template-columns: minmax(0, 1fr) 100px 96px; }
    .aperf-axis,
    .aperf-ticks { display: none; }
    .aperf-summary { text-align: left; }
}

/* ===== LEFT NAV SIDEBAR (PRISM) ===== */
/* Re-skins the black-dashboard left nav. Loaded after black-dashboard.css,
   so these rules win on equal specificity. Scoped via .prism-sidebar so they
   never bleed onto unrelated `.sidebar` containers. */

/* Neutral, like every other surface at rest. This was the largest accent-washed
   area in the app — a full-height column of the skin's hue down the left of
   every page — and it is the single biggest reason picking a skin recoloured
   the furniture rather than the accents. The nav items keep their accent: the
   active item's rail, the icons, and the hover wash are all still the skin, so
   the sidebar reads as skinned without being dyed. */
.sidebar.prism-sidebar {background: linear-gradient(180deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%);border-right: 1px solid var(--prism-border);
    box-shadow: 4px 0 30px rgba(0, 0, 0, 0.25);
    backdrop-filter: blur(6px);
}

.sidebar.prism-sidebar::before,
.sidebar.prism-sidebar::after {
    background: none !important;
    opacity: 0 !important;
}

.white-content .sidebar.prism-sidebar {
    box-shadow: 4px 0 30px rgba(29, 37, 59, 0.06);
}

/* Scrollbar */
.sidebar.prism-sidebar .sidebar-wrapper::-webkit-scrollbar {
    width: 6px;
}

.sidebar.prism-sidebar .sidebar-wrapper::-webkit-scrollbar-track {background: var(--prism-surface-2);
    border-radius: 10px;
}

.sidebar.prism-sidebar .sidebar-wrapper::-webkit-scrollbar-thumb {background: var(--prism-fill-1);
    border-radius: 10px;
}

.sidebar.prism-sidebar .sidebar-wrapper::-webkit-scrollbar-thumb:hover {background: var(--prism-fill-2);
}

/* Nav items */
.sidebar.prism-sidebar .nav {
    margin-top: 0;
    padding: 24px 12px 0;
}

.sidebar.prism-sidebar .nav li > a {
    margin: 4px 0;
    padding: 11px 14px;
    border-radius: 10px;
    background: transparent;color: var(--prism-text-muted);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    box-shadow: none;
    border-left: 3px solid transparent;
    transition: background 0.25s ease, color 0.25s ease, transform 0.25s ease,
                border-color 0.25s ease, box-shadow 0.25s ease;
    display: flex;
    align-items: center;
    gap: 12px;
}

.sidebar.prism-sidebar .nav li > a p {
    margin: 0;
    line-height: 1.2;
    color: inherit;
}

.sidebar.prism-sidebar .nav li > a i,
.sidebar.prism-sidebar .nav li > a svg {
    color: var(--prism-accent-text);
    fill: currentColor;
    font-size: 1.05rem;
    margin-right: 0;
    transition: color 0.25s ease, transform 0.25s ease;
    flex-shrink: 0;
}

.sidebar.prism-sidebar .nav li > a:hover,
.sidebar.prism-sidebar .nav li > a:focus {background: var(--prism-tint-3);color: var(--prism-text);
    transform: translateX(2px);
    box-shadow: none;
}

.sidebar.prism-sidebar .nav li > a:hover i,
.sidebar.prism-sidebar .nav li > a:focus i,
.sidebar.prism-sidebar .nav li > a:hover svg,
.sidebar.prism-sidebar .nav li > a:focus svg {
    color: var(--prism-accent-text);
}

/* Active state */
.sidebar.prism-sidebar .nav li.active > a,
.sidebar.prism-sidebar .nav li.active > a:hover,
.sidebar.prism-sidebar .nav li.active > a:focus {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.22) 0%, rgba(var(--prism-accent2-rgb), 0.18) 100%);
    border-left-color: var(--prism-accent);color: var(--prism-text);
    box-shadow: 0 4px 18px rgba(var(--prism-accent-rgb), 0.25);
    transform: none;
}

.sidebar.prism-sidebar .nav li.active > a i,
.sidebar.prism-sidebar .nav li.active > a svg {
    color: var(--prism-accent-text);
}

/* Kill the legacy black-dashboard ":before" dot indicator */
.sidebar.prism-sidebar .nav li.active > a:not([data-toggle="collapse"])::before,
.sidebar.prism-sidebar .nav li.active > a::before {
    content: none;
    display: none;
}

/* Divider */
.sidebar.prism-sidebar .nav .prism-sidebar-divider {
    list-style: none;
    margin: 12px 4px;
    padding: 0;
}

.sidebar.prism-sidebar .nav .prism-sidebar-divider hr,
.sidebar.prism-sidebar .nav > hr {
    border: none;
    height: 1px;
    margin: 0;
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.3) 50%, transparent 100%);
}

/* Light-mode overrides */

/* Light-mode <p> overrides — black-dashboard sets `.white-content .sidebar p`
   to near-white, and `.sidebar .nav li:hover>a p` / `.active>a p` to solid
   white. We override those at higher specificity so labels stay readable. */
.white-content .sidebar.prism-sidebar .nav li > a p {
    color: rgba(29, 37, 59, 0.7);
}

.white-content .sidebar.prism-sidebar .nav li:hover:not(.active) > a p,
.white-content .sidebar.prism-sidebar .nav li:focus:not(.active) > a p,
.white-content .sidebar.prism-sidebar .nav li:hover:not(.active) > a i,
.white-content .sidebar.prism-sidebar .nav li:focus:not(.active) > a i {
    color: #1d253b;
}

.white-content .sidebar.prism-sidebar .nav li.active > a,
.white-content .sidebar.prism-sidebar .nav li.active > a:hover,
.white-content .sidebar.prism-sidebar .nav li.active > a:focus {
    background: linear-gradient(135deg, rgba(var(--prism-accent-rgb), 0.16) 0%, rgba(var(--prism-accent2-rgb), 0.12) 100%);
    box-shadow: 0 4px 14px rgba(var(--prism-accent-rgb), 0.18);
}

.white-content .sidebar.prism-sidebar .nav li.active > a p,
.white-content .sidebar.prism-sidebar .nav li.active > a:not([data-toggle="collapse"]) p {
    color: #1d253b;
}

.white-content .sidebar.prism-sidebar .nav li.active > a i,
.white-content .sidebar.prism-sidebar .nav li.active > a svg,
.white-content .sidebar.prism-sidebar .nav li.active > a:not([data-toggle="collapse"]) i {
    color: var(--prism-accent-text);
}

.white-content .sidebar.prism-sidebar .nav .prism-sidebar-divider hr,
.white-content .sidebar.prism-sidebar .nav > hr {
    background: linear-gradient(90deg, transparent 0%, rgba(var(--prism-accent-rgb), 0.25) 50%, transparent 100%);
}

/* ===== HINT =====
   A one-time explainer that sits above a feature and then gets out of the way.
   In prism-common rather than the page it debuted on, because "an inline hint"
   is a component and the next one should not be reinvented as another
   .alert-info with a hardcoded blue.

   Left-aligned on purpose. The original was centred, which is fine for one
   line and hard work for three — the eye has to find the start of every line.

   Not `.alert-info`: that is black-dashboard's, and it painted a blue wash and
   a blue left rail regardless of skin or mode. */
.prism-hint {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    text-align: left;
    padding: 14px 16px;
    margin-bottom: 18px;
    border: 1px solid var(--prism-border);
    border-left: 3px solid var(--prism-accent);
    border-radius: var(--prism-radius-panel);
    background: var(--prism-veil-2);
}

.prism-hint[hidden] { display: none; }

.prism-hint-icon {
    flex: none;
    margin-top: 2px;
    font-size: 0.95rem;
    color: var(--prism-accent-text);
}

.prism-hint-body { flex: 1; min-width: 0; }

.prism-hint-lead {
    margin: 0;
    font-size: 0.88rem;
    color: var(--prism-text-2);
    line-height: 1.55;
}

.prism-hint-lead strong { color: var(--prism-text); font-weight: 700; }

.prism-hint-try {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 7px;
    margin: 9px 0 0;
}

.prism-hint-try-label {
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--prism-text-faint);
}

/* These are buttons, and they have to look like it — the point of the hint is
   that you can click them. */
.prism-hint-chip {
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    padding: 3px 12px;
    border-radius: var(--prism-radius-pill);
    border: 1px solid var(--prism-accent-border);
    background: var(--prism-tint-2);
    color: var(--prism-accent-text);
    cursor: pointer;
    transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.prism-hint-chip:hover,
.prism-hint-chip:focus-visible {
    background: var(--prism-tint-4);
    border-color: var(--prism-accent);
    transform: translateY(-1px);
}

.prism-hint-or {
    font-size: 0.8rem;
    color: var(--prism-text-muted);
}

.prism-hint-dismiss {
    flex: none;
    background: none;
    border: 0;
    padding: 4px 6px;
    font-size: 0.78rem;
    line-height: 1;
    color: var(--prism-text-faint);
    cursor: pointer;
    border-radius: 6px;
    transition: color 0.2s ease, background 0.2s ease;
}

.prism-hint-dismiss:hover,
.prism-hint-dismiss:focus-visible {
    color: var(--prism-text);
    background: var(--prism-surface-hover);
}

/* ===== SCROLL CONTAINMENT =====
   Reaching the end of a scrollable region hands the leftover scroll to whatever
   is behind it. That is the browser default, and inside a fixed panel it is
   always wrong: flick to the bottom of the nav and the page underneath keeps
   going, so you look back and the content has moved.

   The right drawer was given `overscroll-behavior: contain` when it was built;
   nothing else was. Every other scrolling region carries it in its OWN rule,
   next to the `overflow` that creates the problem, so the two cannot be
   separated by a rename. This one is the exception because its overflow is
   black-dashboard's -- see `.sidebar-wrapper` at 17378 -- and there is
   no rule of ours to put it in.

   `contain` and not `none`: it stops the chain while leaving the platform's own
   overscroll gesture inside the element, so rubber-banding and pull-to-refresh
   still behave on touch. */
.sidebar.prism-sidebar .sidebar-wrapper {
    overscroll-behavior: contain;
}

/* ===== LEFT SIDEBAR TOGGLE (Floating chevron tab) ===== */

.sidebar.prism-sidebar {
    /* Allow the floating chevron tab to overflow the sidebar's right edge. */
    overflow: visible;
}

.prism-sidebar-toggle {
    position: absolute;
    top: 28px;
    right: -14px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 1px solid rgba(var(--prism-accent-rgb), 0.4);
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%);
    color: #ffffff;
    font-size: 0.75rem;
    line-height: 1;
    padding: 0;
    cursor: pointer;
    z-index: 5;
    box-shadow: 0 6px 18px rgba(var(--prism-accent-rgb), 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
}

.prism-sidebar-toggle:hover,
.prism-sidebar-toggle:focus {
    background: linear-gradient(135deg, var(--prism-accent-light) 0%, var(--prism-accent) 100%);
    box-shadow: 0 8px 22px rgba(var(--prism-accent-rgb), 0.6);
    transform: scale(1.08);
    outline: none;
    color: #ffffff;
}

.prism-sidebar-toggle i {
    transition: transform 0.3s ease;
}

body.sidebar-mini .prism-sidebar-toggle i {
    transform: rotate(180deg);
}

/* ===== MINI-STATE: ICON-ONLY RAIL ===== */
/* Re-uses black-dashboard's `body.sidebar-mini` infra (already wires width 80px,
   hides logo-normal, shifts main-panel padding). These rules layer prism polish
   on top: hide labels cleanly, center icons, restore them on hover-expand. */

@media (min-width: 991px) {
    body.sidebar-mini .sidebar.prism-sidebar .nav {
        padding: 0 8px;
    }

    body.sidebar-mini .sidebar.prism-sidebar .nav li > a {
        justify-content: center;
        padding: 11px 8px;
        gap: 0;
    }

    body.sidebar-mini .sidebar.prism-sidebar .nav li > a p {
        display: none;
    }

    body.sidebar-mini .sidebar.prism-sidebar .nav li > a i,
    body.sidebar-mini .sidebar.prism-sidebar .nav li > a svg {
        margin: 0;
    }

    /* Hover-expand: black-dashboard expands sidebar to 260px on :hover. Restore
       prism nav layout so labels reappear with the icons. */
    body.sidebar-mini .sidebar.prism-sidebar:hover .nav {
        padding: 24px 12px 0;
    }

    body.sidebar-mini .sidebar.prism-sidebar:hover .nav li > a {
        justify-content: flex-start;
        padding: 11px 14px;
        gap: 12px;
    }

    body.sidebar-mini .sidebar.prism-sidebar:hover .nav li > a p {
        display: block;
    }

    /* Push the toggle tab so it always sits at the sidebar's right edge,
       regardless of expand state. (right: -14px is relative to the parent,
       so it follows the sidebar width automatically — no override needed.) */
}

/* On mobile, sidebar-mini doesn't apply (the mobile nav opens via .nav-open),
   so hide the chevron tab to avoid a stray button on small viewports. */
@media (max-width: 991px) {
    .prism-sidebar-toggle {
        display: none;
    }
}

/* ===== TOP NAVBAR (PRISM) ===== */
/* Re-skin of the navbar in [navigation.html]. Keeps black-dashboard markup and
   behavior (dropdowns, modals, mobile toggle) intact. Scoped via .prism-navbar
   so it never bleeds onto unrelated `.navbar` containers. */

.navbar.prism-navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    /* Was rgba(30,30,47,...) -- black-dashboard's #1e1e2f, which stopped being
       anything the page contains once the ambient mesh landed. The token is
       keyed to --prism-bg-base's family instead. */
    background: var(--prism-navbar-glass) !important;
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    /* No border-bottom, and no ::after hairline either. .prism-scroll-progress
       now rides this edge and IS the navbar's bottom rule — its track is a
       full-width 3px line whether or not the page has been scrolled. Keeping
       the old 1px accent line under it would double the edge, and worse, the
       track is only ~10% alpha, so the accent would read THROUGH the track and
       tint the rail the progress fill is measured against. The box-shadow
       below still carries the lift. */
    box-shadow: 0 2px 18px rgba(0, 0, 0, 0.18);
    padding-top: 12px;
    padding-bottom: 12px;
    transition: background 0.3s ease;
}

.navbar.prism-navbar.navbar-transparent {
    background: var(--prism-navbar-glass) !important;
}

.white-content .navbar.prism-navbar,
.white-content .navbar.prism-navbar.navbar-transparent {
    box-shadow: 0 2px 18px rgba(29, 37, 59, 0.06);
}

/* Brand */
.navbar.prism-navbar .navbar-brand {
    /* `position: static` is not tidying. black-dashboard sets
       `.navbar .navbar-brand { position: fixed; margin-left: 17px }`
       (15881), which was survivable while the brand was a lone anchor and is
       not now that it sits in a flex lockup beside the mark: fixed takes it out
       of flow entirely, so it pins itself to the viewport and lands on top of
       the logo instead of next to it. */
    position: static;
    margin-left: 0;
    margin-top: 0;
    padding: 0;
    /* --prism-wordmark, not the accent pair this used to read. The brand is
       identity; the accent is a preference. */
    background: var(--prism-wordmark);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    /* Poppins, which is what "Doshi Investments LLC" rendered in — the brand
       never set a family, so it took the body face, and moving the body face
       moved the brand with it. Weight 700, matching. */
    font-family: var(--prism-font-title);
    font-size: 1.28rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    transition: opacity 0.25s ease;
}

.navbar.prism-navbar .navbar-brand:hover {
    opacity: 0.85;
}

/* The mark, and the same rule: its glow was rgba(--prism-accent-rgb), so the
   logo threw a green shadow on the green skin. A violet one, fixed, matching
   the mark's own middle stop. */
.navbar.prism-navbar .navbar-wrapper img,
.prism-brand-mark {
    filter: drop-shadow(0 2px 7px rgba(139, 92, 246, 0.45));
}

/* The glow is the Prism mark's own middle stop cast back onto it, so it reads
   as the logo lighting its surroundings. Under any other mark it is just a
   coloured halo of the wrong hue — a violet bloom around a gold "D". */
body[data-brand="doshi"] .navbar.prism-navbar .navbar-wrapper img,
body[data-brand="doshi"] .prism-brand-mark {
    filter: drop-shadow(0 2px 7px rgba(184, 134, 11, 0.40));
}

/* ===== BRAND LOCKUP (mark + wordmark) =====
   One component, two placements. The mark is an SVG so a single file serves
   both sizes; only the height differs. */
.prism-brand-lockup {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    text-decoration: none;
}

.prism-brand-lockup:hover { text-decoration: none; }

.prism-brand-mark {
    /* Was a 26px PNG. The mark is the product's only piece of identity in the
       chrome and it was smaller than the notification bell next to it. */
    height: 34px;
    width: auto;
    display: block;
    flex: none;
}

/* ===== BRAND MARK: PER-COMPANY GEOMETRY =====
   Keyed off body[data-brand], set from the same BRAND preset the templates
   read (app/base/brand.py), so the mark's sizing cannot drift from the mark.

   Height, not width, is what the rule above locks — which makes it a property
   of the mark's aspect ratio, not a constant. Prism's SVG is roughly 1:2, tall
   and narrow, so 34px costs ~17px of width. Doshi's is a wide gold "D" at
   roughly 5:6, so the same 34px is nearly twice as wide and pushes the
   navbar's centred search out of the flex row. It comes down instead of the
   layout going sideways. */
body[data-brand="doshi"] .prism-brand-mark { height: 24px; }
body[data-brand="doshi"] .footer .prism-footer-brand .prism-brand-mark { height: 22px; }

/* The rule above the footer, matching /home's `footer { border-top: 1px solid
   var(--px-border) }`. Without it the footer's brand floats loose under the
   last card with nothing marking where the content stops — which is exactly
   what the border does on the landing page.

   The generous margin-top is doing the same job as the landing page's 100px:
   the line only reads as a boundary if there is space either side of it. The
   vendor's `.footer { padding: 24px 0 24px 250px }` stays, since that left
   padding is what clears the sidebar — on the pages that have one. See the
   SIGNED-OUT CHROME block below for the other case. */
body .footer {
    border-top: 1px solid var(--prism-border);
    margin-top: 56px;
    padding-top: 30px;
}

/* Centred, and centred by the GRID rather than by margin:auto — the footer's
   own `ul.nav` is black-dashboard's and floats left, so a centred block above
   it would still sit next to whatever that list renders. Putting the brand in
   its own row is what makes "centre" mean the page rather than the space left
   over. */
.footer .prism-footer-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 11px;
    width: 100%;
    padding-bottom: 6px;
}

.footer .prism-footer-brand .prism-brand-mark {
    height: 30px;
}

.prism-footer-wordmark {
    /* Same face as the navbar's — it is the same wordmark. */
    font-family: var(--prism-font-title);
    font-size: 1.12rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    background: var(--prism-wordmark);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}

/* The footer's nav row centres under the brand rather than hugging the left,
   so the two read as one stacked lockup instead of two left-aligned things.

   `float: none` and `display: flex` are the load-bearing pair. black-dashboard
   gives `.footer .nav` `display: inline-block; float: left` (14600), and
   justify-content does nothing to either — centring it needs the float cleared
   and a flex context, not an alignment property. `body` prefix to out-specify
   the vendor's 0-2-0. */
body .footer .container-fluid {
    display: flex;
    flex-direction: column;
    align-items: center;
}

body .footer .nav {
    display: flex;
    float: none;
    flex-wrap: wrap;
    justify-content: center;
    padding-left: 0;
}

/* Legal links: quiet, evenly spaced, and separated by a rule rather than by a
   bullet character, so the row reads as one line at any width. */
.footer .prism-footer-links {
    gap: 0;
    margin-top: 2px;
}

.footer .prism-footer-links .nav-item + .nav-item {
    border-left: 1px solid var(--prism-border);
}

.footer .prism-footer-links a {
    display: block;
    padding: 0 14px;
    font-size: 0.82rem;
    color: var(--prism-text-muted);
    transition: color 0.2s ease;
}

.footer .prism-footer-links a:hover,
.footer .prism-footer-links a:focus {
    color: var(--prism-text);
    text-decoration: none;
}

/* The security line. Deliberately present in-app and not only on the marketing
   page: the screen where someone links a bank account is where it means
   something. */
.footer .prism-footer-trust {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 8px 0 0;
    font-size: 0.78rem;
    color: var(--prism-text-faint);
}

.footer .prism-footer-trust i {
    font-size: 0.72rem;
    color: var(--prism-success);
}

/* It is an <a> to /privacy-and-security, so it needs a hover state — a link
   that looks exactly like the static text beside it is a link nobody clicks.
   The transition matches .prism-footer-links above. */
.footer a.prism-footer-trust {
    transition: color 0.2s ease;
}

/* ===== SIGNED-OUT CHROME ===== */

/* The left sidebar is app navigation, so includes/sidebar.html renders nothing
   for a visitor who has not signed in — on /login, /terms,
   /privacy-and-security and every error page, all of which extend the app
   layout.

   Deleting the markup is only half of it. The sidebar is `position: fixed`, so
   it is out of flow and the gutter it appears to occupy is really padding on
   the two elements that clear it: `.main-panel > .content { padding-left:
   280px }` (black-dashboard.css:17839) and `.footer { padding-left: 250px }`
   (:14592). Both survive the sidebar's removal, and what is left is a dead
   strip that pushes the login card, both legal documents and every error stage
   about 125px right of centre — each of which centres *inside* the padded box,
   so every one of them looks subtly wrong rather than obviously broken.

   Scoped to >= 992px because below that the vendor already collapses both
   (:18051 and :14640) for its off-canvas sidebar, and re-stating them here
   would out-specify the 768px step that narrows .content to 15px.

   `body.px-signed-out` comes from layouts/base.html. The `.prism-navbar-center`
   calc further down compensates for the same 280px, and deliberately does NOT
   need a companion rule here: the search pill it positions lives inside
   navigation.html's authenticated block, so it never renders on a page that
   carries this class. */
@media (min-width: 992px) {
    body.px-signed-out .main-panel > .content {
        padding-left: 30px;
    }

    body.px-signed-out .footer {
        padding-left: 0;
    }
}

/* Mobile hamburger */
.navbar.prism-navbar .navbar-toggler-bar {
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent2) 100%);
}

/* ===== SEARCH PILL ===== */

/* Make the navbar's container-fluid an explicit positioning context so the
   centered wrapper can be absolutely positioned relative to the full navbar
   width (rather than falling back to .navbar-absolute, which has shrink-to-fit
   width and can place the wrapper off-center). */
.navbar.prism-navbar > .container-fluid {
    position: relative;
}

/* Center wrapper sits absolutely centered in the navbar at desktop breakpoints,
   so the search pill is true-centered regardless of how wide the brand on the
   left or the right-aligned icon stack is. On mobile (<992px) it falls back to
   normal in-flow rendering inside the collapsed menu. */
.prism-navbar-center {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    padding: 8px 0;
}

@media (min-width: 992px) {
    /* Visually center the search pill against the CONTENT AREA (post-sidebar)
       rather than the full viewport, so it lines up with page titles like the
       Welcome heading. The .main-panel spans the full viewport width, but
       .main-panel > .content has padding-left: 280px (sidebar offset) and
       padding-right: 30px, so its visual center is at:
           (280 + viewport - 30) / 2 = viewport/2 + 125px
       In sidebar-mini mode the offsets become 130/30, giving viewport/2 + 50px.
       The 280px is signed-in-only now (see SIGNED-OUT CHROME above), but so is
       the pill — it renders inside navigation.html's authenticated block — so
       the two can never disagree. */
    .navbar.prism-navbar .prism-navbar-center {
        position: absolute;
        left: calc(50% + 125px);
        top: 50%;
        transform: translate(-50%, -50%);
        width: auto;
        padding: 0;
        z-index: 1;
        pointer-events: none; /* let clicks pass through the wrapper */
        transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
    }
    body.sidebar-mini .navbar.prism-navbar .prism-navbar-center {
        left: calc(50% + 50px);
    }
    /* When the mini sidebar hover-expands back to 260px, content padding-left
       stays at 130px (black-dashboard doesn't animate it), so we keep the
       sidebar-mini offset of +50px. No further adjustment needed. */
    .navbar.prism-navbar .prism-navbar-center > .prism-navbar-search {
        pointer-events: auto; /* but the pill itself stays clickable */
    }
}

.navbar.prism-navbar .prism-navbar-search {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-width: 280px;
    padding: 8px 10px 8px 14px;background: var(--prism-tint-2);border: 1px solid var(--prism-accent-border);
    border-radius: 999px;color: var(--prism-text-dim);
    font-size: 0.85rem;
    text-align: left;
    cursor: pointer;
    transition: background 0.25s ease, border-color 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
}

.navbar.prism-navbar .prism-navbar-search:hover,
.navbar.prism-navbar .prism-navbar-search:focus {background: var(--prism-tint-4);border-color: var(--prism-accent-border);color: var(--prism-text);
    box-shadow: 0 4px 18px rgba(var(--prism-accent-rgb), 0.18);
    outline: none;
}

.navbar.prism-navbar .prism-navbar-search-icon {
    font-size: 1rem;
    color: var(--prism-accent-text);
    flex-shrink: 0;
    margin: 0;
}

.navbar.prism-navbar .prism-navbar-search-placeholder {
    flex: 1;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.navbar.prism-navbar .prism-navbar-search-kbd {
    display: inline-flex;
    gap: 2px;
    flex-shrink: 0;
}

.navbar.prism-navbar .prism-navbar-search-kbd kbd {background: var(--prism-surface-hover);border: 1px solid var(--prism-border-strong);
    border-radius: 4px;
    padding: 1px 6px;
    font-size: 0.65rem;
    line-height: 1.4;color: var(--prism-text-muted);
    font-family: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace;
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset;
}

/* Light mode */

.white-content .navbar.prism-navbar .prism-navbar-search:hover,
.white-content .navbar.prism-navbar .prism-navbar-search:focus {
    box-shadow: 0 4px 14px rgba(var(--prism-accent-rgb), 0.12);
}

.white-content .navbar.prism-navbar .prism-navbar-search-kbd kbd {
    box-shadow: none;
}

/* Mobile: collapse the search pill so it's just an icon button */
@media (max-width: 991px) {
    .navbar.prism-navbar .prism-navbar-search {
        min-width: 0;
        padding: 8px;
    }
    .navbar.prism-navbar .prism-navbar-search-placeholder,
    .navbar.prism-navbar .prism-navbar-search-kbd {
        display: none;
    }
}

/* ===== ICON NAV LINKS (bell, profile chevron) ===== */
/* Vertically center all navbar-nav items along a shared baseline so the bell
   icon, profile avatar, and right-sidebar toggle line up regardless of their
   intrinsic heights. */
.navbar.prism-navbar .navbar-nav {
    align-items: center;
}

.navbar.prism-navbar .navbar-nav .nav-item {
    display: flex;
    align-items: center;
}

.navbar.prism-navbar .navbar-nav .nav-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-radius: 10px;color: var(--prism-text-muted) !important;
    transition: background 0.25s ease, color 0.25s ease;
}

.navbar.prism-navbar .navbar-nav .nav-link:hover,
.navbar.prism-navbar .navbar-nav .nav-link:focus {background: var(--prism-tint-3);color: var(--prism-text) !important;
}

.navbar.prism-navbar .navbar-nav .nav-link i {
    font-size: 1.1rem;
    color: var(--prism-accent-text);
    transition: color 0.25s ease;
}

.navbar.prism-navbar .navbar-nav .nav-link:hover i {
    color: var(--prism-accent-text);
}

/* Notification count badge — pinned over the top-right corner of the bell.
   Sized for 1–3 character counts ("9", "42", "99+"); JS caps the display at
   "99+" so the badge never overflows. */
.navbar.prism-navbar .navbar-nav .nav-link {
    position: relative;
}

.navbar.prism-navbar .prism-notification-trigger > i {
    font-size: 1.05rem;
}

/* Badge sits at the top-right CORNER of the trigger and is then translated
   ~35% outward so it floats just above-and-right of the bell instead of
   landing on top of it. min-width grows the pill for "99+" without losing
   the clean corner anchor. */
.navbar.prism-navbar .prism-notification-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    transform: translate(40%, -40%);
    transform-origin: center;
    min-width: 17px;
    height: 17px;
    padding: 0 5px;
    border-radius: 9px;
    background: linear-gradient(135deg, #fd5d93 0%, #ff7aa8 100%);
    color: #ffffff;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 0 2px var(--prism-bg-base),
                0 4px 10px rgba(253, 93, 147, 0.45);
    pointer-events: none;
    z-index: 2;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    transition: transform 0.2s ease;
}

/* An empty badge is not a badge. notifications.js states the unread count by
   writing the number into this span and clears it by emptying it, so the hiding
   has to happen here -- jQuery's .hide() writes an inline `display: none`, and
   the span carries `.d-lg-inline-flex { display: inline-flex !important }` from
   black-dashboard.css, which outranks a non-important inline declaration. That
   is why the badge used to keep a stale count until a reload. This selector is
   (0,4,0) + !important, which does outrank it. It also stops the badge painting
   as a blank pink pill in the moment before the first count poll answers. */
.navbar.prism-navbar .prism-notification-badge:empty {
    display: none !important;
}

.navbar.prism-navbar .prism-notification-trigger:hover .prism-notification-badge {
    transform: translate(40%, -40%) scale(1.1);
}

.white-content .navbar.prism-navbar .prism-notification-badge {
    box-shadow: 0 0 0 2px #ffffff, 0 4px 10px rgba(253, 93, 147, 0.35);
}

.white-content .navbar.prism-navbar .notification-count-dot {
    box-shadow: 0 0 0 2px #ffffff, 0 0 10px rgba(253, 93, 147, 0.4);
}

/* ===== AVATAR ===== */
.navbar.prism-navbar .photo {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    overflow: hidden;
    padding: 2px;
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent2) 100%);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.navbar.prism-navbar .photo img,
.navbar.prism-navbar .photo span {
    width: 100% !important;
    height: 100% !important;
    border-radius: 50%;
    display: block;
    object-fit: cover;
    margin: 0 !important;
}

.navbar.prism-navbar .nav-link:hover .photo {
    transform: scale(1.05);
    box-shadow: 0 6px 18px rgba(var(--prism-accent-rgb), 0.4);
}

.navbar.prism-navbar .caret {color: var(--prism-text-dim);
    margin-left: 2px;
}

/* ===== DROPDOWN MENUS ===== */
.navbar.prism-navbar .dropdown-menu.dropdown-navbar {
    /* --prism-panel, not a literal pair. This is the floating-chrome token and
       it already flips; the light override below existed only to undo this. */
    background: var(--prism-panel);border: 1px solid var(--prism-accent-border);
    border-radius: 12px;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45),
                0 0 0 1px rgba(var(--prism-accent-rgb), 0.1) inset;
    padding: 8px;
    margin-top: 12px;
    overflow: hidden;
}

.navbar.prism-navbar .dropdown-menu.dropdown-navbar::before,
.navbar.prism-navbar .dropdown-menu.dropdown-navbar::after {
    display: none !important;
}

.navbar.prism-navbar .dropdown-menu .dropdown-header {
    padding: 10px 14px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;color: var(--prism-text) !important;
    background: rgba(var(--prism-accent-rgb), 0.08);
    border-radius: 8px;
    margin-bottom: 6px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.navbar.prism-navbar .dropdown-menu .dropdown-header a {
    color: var(--prism-accent-text) !important;
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
}

.navbar.prism-navbar .dropdown-menu .dropdown-item,
.navbar.prism-navbar .dropdown-menu .nav-link a {
    padding: 9px 14px;
    border-radius: 8px;color: var(--prism-text-muted) !important;
    font-size: 0.9rem;
    transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}

.navbar.prism-navbar .dropdown-menu .dropdown-item:hover,
.navbar.prism-navbar .dropdown-menu .dropdown-item:focus,
.navbar.prism-navbar .dropdown-menu .nav-link a:hover {background: var(--prism-tint-4);color: var(--prism-text) !important;
    transform: translateX(2px);
}

.navbar.prism-navbar .dropdown-menu .dropdown-item.text-danger,
.navbar.prism-navbar .dropdown-menu .dropdown-item.text-danger:hover {
    color: var(--prism-danger) !important;
}

.navbar.prism-navbar .dropdown-menu .dropdown-item.text-danger:hover {
    background: rgba(253, 93, 147, 0.15);
}

.navbar.prism-navbar .dropdown-menu .dropdown-divider {
    margin: 6px 0;border-top: 1px solid var(--prism-accent-border);
}

.navbar.prism-navbar .dropdown-menu .nav-link {
    padding: 0;
}

.navbar.prism-navbar .dropdown-menu .notifications-container {
    margin: 4px 0;
}

/* Light mode dropdowns */
.white-content .navbar.prism-navbar .dropdown-menu.dropdown-navbar {
    background: var(--prism-panel);
    box-shadow: 0 16px 40px rgba(29, 37, 59, 0.12),
                0 0 0 1px rgba(var(--prism-accent-rgb), 0.08) inset;
}

/* ==================================================================
   Transaction list rows (.tx-list-*) — shared between the
   "Unreviewed Transactions" widget on index.html and the
   All Transactions V2 page. The .unreviewed-* selectors are kept as
   aliases so the index.html widget continues to render unchanged.
   ================================================================== */
.unreviewed-transactions-container,
.tx-list-container {
    max-height: 600px;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;

    overscroll-behavior: contain;}

/* Transaction-list pages (asset-transactions / transactions): let the list's own
   scroll area fill the viewport so many more rows are visible once it scrolls
   into view. The dashboard's .unreviewed-transactions-container keeps the
   600px cap from the shared rule above. */
.tx-list-container {
    max-height: calc(100vh - 160px);
}

.unreviewed-date-group,
.tx-list-date-group {
    margin-bottom: 4px;
}

.unreviewed-day-label,
.tx-list-day-label {
    font-size: 0.8em;
    font-weight: 600;
    letter-spacing: 0.6px;
    color: var(--prism-accent-text);
    text-transform: uppercase;
    margin-top: 14px;
    margin-bottom: 4px;
    padding: 0 4px;
}

.unreviewed-row,
.tx-list-row {
    display: grid;
    /* check | name | account | spacer (absorbs slack) | origin | cat (fixed) | amount (fixed) | dot
       Fixed widths on cat and amount keep those columns aligned across every row. */
    grid-template-columns: 22px minmax(0, 480px) minmax(0, auto) 1fr 100px 240px 110px 8px;
    gap: 10px;
    align-items: center;
    padding: 10px 8px;border-bottom: 1px solid var(--prism-border-faint);
    cursor: pointer;
    border-radius: 6px;
    transition: background-color 0.15s ease;
}
.unreviewed-row > .unreviewed-row-origin,
.tx-list-row > .tx-list-row-origin { grid-column: 5; }
.unreviewed-row > .unreviewed-row-cat,
.tx-list-row > .tx-list-row-cat    { grid-column: 6; }
.unreviewed-row > .unreviewed-row-amount,
.tx-list-row > .tx-list-row-amount { grid-column: 7; }
.unreviewed-row > .unreviewed-row-dot,
.tx-list-row > .tx-list-row-dot    { grid-column: 8; }

.unreviewed-row-origin,
.tx-list-row-origin {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    min-width: 0;
}

.unreviewed-row:hover,
.tx-list-row:hover {background-color: var(--prism-tint-2);
}

.unreviewed-row:last-child,
.tx-list-row:last-child {
    border-bottom: none;
}

.unreviewed-row-check,
.tx-list-row-check {
    display: flex;
    align-items: center;
    justify-content: center;
}

.unreviewed-row-check input[type="checkbox"],
.tx-list-row-check input[type="checkbox"] {
    width: 16px;
    height: 16px;
    cursor: pointer;
    accent-color: var(--prism-accent);
    margin: 0;
}

.unreviewed-row-name-wrap,
.tx-list-row-name-wrap {
    min-width: 0;
    max-width: 480px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.unreviewed-row-name,
.tx-list-row-name {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 1em;color: var(--prism-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.unreviewed-row-name-text,
.tx-list-row-name-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Small grey subtitle line beneath the merchant name (asset link, etc.) */
.tx-list-row-subline {
    font-size: 0.78em;color: var(--prism-text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.tx-list-row-subline a {
    color: inherit;
    text-decoration: none;
}
.tx-list-row-subline a:hover {
    color: var(--prism-accent-text);
    text-decoration: none;
}

/* Tiny inline indicator icons (invoice / note) inside the name line.
   Read-only — the row click opens the sidebar where they're actionable. */
.tx-list-row-icon {
    font-size: 0.78em;color: var(--prism-text-dim);
    flex-shrink: 0;
}
.tx-list-row-icon.has-invoice { color: #ec4899; }
.tx-list-row-icon.has-note    { color: #6366f1; }

.unreviewed-row-account,
.tx-list-row-account {
    display: flex;
    align-items: center;
    min-width: 0;
    white-space: nowrap;
}

.unreviewed-row-cat,
.tx-list-row-cat {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 6px;
    flex-wrap: wrap;
    min-width: 0;
    /* No overflow:hidden — long pill pairs wrap to a second line inside the
       cell instead of being clipped. The cell is column-aligned across rows
       via the fixed grid track; row height grows to fit wrapped content. */
}
.tx-list-row-cat .category-badges-wrap,
.unreviewed-row-cat .category-badges-wrap {
    max-width: 100%;
    /* Inherit the base .category-badges-wrap { flex-wrap: wrap } so pills
       break onto a second line when the pair exceeds the cat column. */
}

.unreviewed-row-amount,
.tx-list-row-amount {
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
    font-size: 1em;
}

.unreviewed-row-amount.expense,
.tx-list-row-amount.expense { color: var(--prism-danger); }
.unreviewed-row-amount.income,
.tx-list-row-amount.income  { color: var(--prism-success); }

.unreviewed-row-dot,
.tx-list-row-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--prism-accent);
    display: inline-block;
    justify-self: end;
}

/* Bank-suggestion: clickable purple pill matching .tx-origin-chip palette. */
.unreviewed-tx-category-wrap.bank-suggestion,
.tx-list-tx-category-wrap.bank-suggestion {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 8px;
    border-radius: 10px;
    background-color: rgba(155, 89, 182, 0.12);
    border: 1px dashed rgba(155, 89, 182, 0.4);
    transition: all 0.2s ease;
    cursor: pointer;
}

.unreviewed-tx-category-wrap.bank-suggestion:hover,
.tx-list-tx-category-wrap.bank-suggestion:hover {
    background-color: rgba(155, 89, 182, 0.22);
    border-color: rgba(155, 89, 182, 0.8);
}

/* Status variants for .tx-origin-chip used by the all-transactions list.
   Color is set inline elsewhere (auto-categorized origins); these classes
   provide a fixed palette for Pending and Split badges that the list uses
   when no auto-origin chip applies. */
.tx-origin-chip.is-pending {
    color: #f59e0b;
    background: rgba(245, 158, 11, 0.12);
    border: 1px solid rgba(245, 158, 11, 0.4);
}
.tx-origin-chip.is-split {
    color: #9b59b6;
    background: rgba(155, 89, 182, 0.12);
    border: 1px solid rgba(155, 89, 182, 0.4);
}

/* Bulk-action bar: appears when one or more checkboxes are ticked. */
.unreviewed-bulk-bar,
.tx-list-bulk-bar {
    display: none;
    position: sticky;
    top: 0;
    z-index: 20;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 10px;
    border-radius: 8px;
    background: rgba(var(--prism-accent-rgb), 0.12);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.4);
    font-size: 0.95em;
}

.unreviewed-bulk-bar.is-visible,
.tx-list-bulk-bar.is-visible {
    display: flex;
}

.unreviewed-bulk-bar-count,
.tx-list-bulk-bar-count {
    color: var(--prism-accent-text);
    font-weight: 600;
}

.unreviewed-bulk-bar-actions,
.tx-list-bulk-bar-actions {
    display: flex;
    gap: 8px;
}

/* Empty state for the list (no rows match filters). */
.tx-list-empty-state {
    text-align: center;
    padding: 40px 20px;color: var(--prism-text-dim);
}
.tx-list-empty-state i {
    font-size: 2em;
    color: var(--prism-accent-text);
    margin-bottom: 10px;
    display: block;
}

/* Light mode overrides */

.white-content .unreviewed-row-account,
.white-content .tx-list-row-account {
    color: rgba(0, 0, 0, 0.5);
}

/* Small screens: collapse account + origin (matches the unreviewed widget). */
@media (max-width: 1199px) {
    .unreviewed-row,
    .tx-list-row {
        grid-template-columns: 22px minmax(0, 1fr) 0 1fr 0 auto auto 8px;
        gap: 8px;
        padding: 8px 6px;
    }
    .unreviewed-row-account,
    .tx-list-row-account,
    .unreviewed-row-origin,
    .tx-list-row-origin {
        display: none;
    }
}

/* Medium screens */
@media (max-width: 768px) {
    .unreviewed-row-name-wrap,
    .tx-list-row-name-wrap { max-width: 280px; }
    .unreviewed-row-account,
    .tx-list-row-account   { max-width: 180px; }
}

/* All Transactions list (.tx-list-row only): fix every column EXCEPT name
   so the account / origin / category / amount columns line up across rows
   AND there is no visible gap between them. The name column is the only
   1fr track, so it absorbs all leftover row width — the slack lives
   invisibly inside the name slot (left of the wrapped/truncated text)
   instead of as a gap between visible items. The unreviewed widget on
   index.html keeps its 480px name slot via the shared rule above. */
@media (min-width: 1200px) {
    /* Name is a CAPPED track (not 1fr) so it hugs its content: short
       descriptions shrink the name column and pull account/origin/cat right up
       next to it. A flexible 1fr SPACER (track 6) then absorbs all leftover
       width, keeping the amount flush-right. This kills the old giant gap that
       a `minmax(0, 1fr)` name track created after short descriptions. */
    .tx-list-row {
        /* FIXED name/account/origin tracks so the category column starts at the
           SAME x on every row (aligned, scannable). A `minmax(0, N)` track
           fills to N, giving consistent widths; the origin track reserves space
           even when a row has no origin chip, so the category never shifts. The
           1fr spacer (track 6) absorbs slack and keeps the amount flush-right.
           Category is content-sized so it stays tight but always starts aligned. */
        grid-template-columns: 22px minmax(0, 360px) 160px 88px minmax(0, auto) 1fr 100px 8px;
        /* Denser rows so more transactions are visible at once (dashboard's
           shared .unreviewed-row widget keeps the roomier base padding). */
        padding: 6px 8px;
        gap: 8px;
    }
    .tx-list-row .tx-list-row-name-wrap {
        max-width: 360px;
    }
    .tx-list-row .tx-list-row-account {
        max-width: 160px;
        overflow: hidden;
    }
    /* Long categories clip at the column boundary instead of overflowing into
       the amount column. */
    .tx-list-row .tx-list-row-cat {
        overflow: hidden;
    }
    /* check(1) name(2) account(3) origin(4) cat(5) auto-place in DOM order into
       cols 1-5; pin amount/dot to the trailing tracks so they skip the empty
       1fr spacer at track 6. */
    .tx-list-row > .tx-list-row-origin,
    .tx-list-row > .tx-list-row-cat {
        grid-column: auto;
    }
    .tx-list-row > .tx-list-row-amount { grid-column: 7; }
    .tx-list-row > .tx-list-row-dot    { grid-column: 8; }
}

/* =====================================================================
   Right-sidebar Filter Panel — funnel button (in search bar), per-section
   pill grids, custom-date reveal. Used by transactions.html and
   asset-transactions.html with `RightSidebar.openTransactionFilters(...)`.
   Lifted from transactions.html so all transaction pages share it.
   ===================================================================== */
    /* Funnel button (in search bar) */
  .smart-search-filter-btn {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: rgba(var(--prism-accent-rgb), 0.1);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.25);
    color: var(--prism-text-muted);
    cursor: pointer;
    flex-shrink: 0;
    z-index: 10;
    transition: all 0.2s ease;
  }
  .smart-search-filter-btn:hover {
    background: rgba(var(--prism-accent-rgb), 0.18);
    color: #fff;
  }
  .smart-search-filter-btn.has-active-filters {
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%);
    border-color: rgba(var(--prism-accent-light-rgb), 0.95);
    color: #fff;
    box-shadow: 0 4px 14px rgba(var(--prism-accent-rgb), 0.45);
  }
  .white-content .smart-search-filter-btn {
    color: rgba(34, 42, 66, 0.7);
  }
  .filter-count-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 10px;
    background-color: #ef4444;
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    display: none;
    align-items: center;
    justify-content: center;
    line-height: 1;
  }
  .filter-count-badge.visible {
    display: inline-flex;
  }

  /* Overflow chip ("+N more") */
  .active-filter-chip.chip-overflow {
    background-color: rgba(168, 85, 247, 0.2);
    color: #a855f7;
    border: 1px solid rgba(168, 85, 247, 0.4);
    cursor: pointer;
  }
  .active-filter-chip.chip-overflow:hover {
    background-color: rgba(168, 85, 247, 0.35);
  }

  /* ARMED FILTER CHIP (keyboard removal)
     ------------------------------------------------------------------
     Backspace on an empty search field arms the last chip; a second press
     removes it. prism-filter-chips.js owns the behaviour; this is the state
     it paints. Lives here rather than in asset-transactions.html / transactions.html
     because both pages need it and both page-local-CSS ratchets in
     tests/test_page_local_css.py are full.

     A ring, not a colour. The chips are fourteen hardcoded hues, solid-filled
     on the asset page and tinted on all-transactions, so any change to background,
     border or text is invisible on some of them. Two box-shadows -- a
     bar-coloured gap, then a high-contrast ring -- read on every hue and flip
     with light mode for free.

     box-shadow, never outline + outline-offset: it draws outside the border box
     without participating in layout, so arming cannot reflow a bar that is
     already `flex-wrap: wrap`. `.active-filter-chip.is-armed` is (0,2,0) and
     beats the page-local `.active-filter-chip` (0,1,0) whatever order the
     sheets load in, which is what lets the base look stay where it is. */
  .active-filter-chip.is-armed {
    /* The gap layer MUST be a flat colour token. --prism-panel is a
       linear-gradient, and box-shadow takes a <color> per layer -- substituting
       a gradient made the WHOLE declaration invalid, outer ring included, so
       the armed chip had no visible indicator at all on either page while every
       test still passed (they asserted the class, never the paint).
       --prism-surface-raised is that gradient's own top stop in both modes, so
       the gap reads as the bar behind it and still flips with light mode. */
    box-shadow:
      0 0 0 2px var(--prism-surface-raised),
      0 0 0 4px var(--prism-text);
    filter: brightness(1.06);
  }
  .active-filter-chip.is-armed .remove-filter {
    opacity: 1;
  }

  /* The x is a real <button> now -- it was an <i>, so it had no accessible name
     and no keyboard path. Reset the UA button box so it sits exactly where the
     icon used to; the pages' own `.active-filter-chip .remove-filter` rules
     still supply cursor, margin and the hover opacity. */
  .active-filter-chip .remove-filter {
    appearance: none;
    -webkit-appearance: none;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* An explicit box, so the control is hittable even if the icon font has
       not loaded. As an <i> it took its whole size from the glyph: no Font
       Awesome, no target at all -- and `is_visible` still answers yes for a
       0x0 element, so a test could report a control nobody can click as
       present and correct. Same lesson as the drawer's rename pencil. */
    width: 14px;
    height: 14px;
    flex: none;
  }
  .active-filter-chip .chip-icon {
    display: inline-flex;
    align-items: center;
  }

  /* Filter panel (rendered inside right sidebar) */
  .filter-panel-footer {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid rgba(var(--prism-accent-rgb), 0.18);
  }

  .filter-section {
    margin-bottom: 18px;
  }
  .filter-section:last-child {
    margin-bottom: 0;
  }
  .filter-section-title {
    font-size: var(--prism-fs-xs);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(235, 245, 255, 0.65);
    margin-bottom: 10px;
  }
  .white-content .filter-section-title {
    color: rgba(29, 37, 59, 0.65);
  }
  .filter-section-sublabel {
    font-size: var(--prism-fs-2xs);
    font-weight: 600;
    color: rgba(235, 245, 255, 0.55);
    margin: 10px 0 6px;
    letter-spacing: 0.06em;
  }
  .white-content .filter-section-sublabel {
    color: rgba(29, 37, 59, 0.55);
  }

  /* Filter pill grid (replaces dropdowns) */
  .filter-pill-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  .filter-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 6px 12px;
    border-radius: 999px;background: var(--prism-tint-2);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.25);
    color: rgba(235, 245, 255, 0.78);
    font-size: var(--prism-fs-xs);
    font-weight: 500;
    cursor: pointer;
    transition: all 0.18s ease;
    user-select: none;
  }
  .filter-pill:hover {background: var(--prism-tint-4);color: var(--prism-text);
  }
  .filter-pill.active {
    background: linear-gradient(135deg, var(--prism-accent) 0%, var(--prism-accent-light) 100%);
    border-color: rgba(var(--prism-accent-light-rgb), 0.95);
    color: #fff;
    box-shadow: 0 4px 12px rgba(var(--prism-accent-rgb), 0.35);
  }
  .white-content .filter-pill {
    color: rgba(29, 37, 59, 0.78);
  }
  
  

  /* Custom date sub-section reveal */
  .filter-custom-date {
    display: none;
    margin-top: 10px;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
  }
  .filter-custom-date.visible {
    display: flex;
  }
  .filter-custom-date input[type="date"] {
    flex: 1;
    min-width: 140px;
  }

  /* Modal hint line */
  .filter-modal-hint {
    font-size: 12px;
    color: rgba(235, 245, 255, 0.55);
    background: rgba(168, 85, 247, 0.08);
    border: 1px solid rgba(168, 85, 247, 0.2);
    border-radius: 10px;
    padding: 10px 14px;
    margin-top: 4px;
  }
  .filter-modal-hint i {
    color: #a855f7;
    margin-right: 6px;
  }
  .white-content .filter-modal-hint {
    color: rgba(29, 37, 59, 0.65);
  }

/* ===== MAIN PANEL BACKGROUND =====
   Deliberately empty. The rule that used to live here painted .main-panel with
   two accent radials over an opaque #1e1e2f — a two-lobe ancestor of the mesh
   that the PRISM AMBIENT BACKGROUND section at the bottom of this file
   replaced with a three-lobe one on body::before.

   It has been dead since that landed: `body .main-panel` (0-0-2) beats a bare
   `.main-panel` (0-0-1) and appears later, so nothing rendered. Leaving it was
   worse than useless — it was a landmine. Reorder or delete the block at the
   bottom and the opaque #1e1e2f comes straight back, hiding the entire ambient
   mesh behind the very colour that section exists to get rid of, with no
   failing test to say so. Removed rather than corrected: the background belongs
   in one place, and that place is body::before. */

/* ===== SKIN PICKER (navbar profile dropdown) =====
   A wrapping row of circular swatches, one per entry in app/base/theme.py —
   ten today. Each swatch is painted with its OWN skin's accent, hardcoded, so
   that every one shows the colour you would get by clicking it rather than the
   colour the page happens to be right now. The active one gets a ring via
   .is-active. (This comment said "six" for two skins longer than it was true;
   the count now lives in theme.py and tests/test_theme_skins.py enforces that
   the swatches and the list agree.) */
.prism-skin-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 6px 0 2px;
}

.prism-skin-swatch {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
    border: 2px solid transparent;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    transition: transform 0.2s ease, border-color 0.2s ease;
}

.prism-skin-swatch:hover {
    transform: scale(1.15);
}

.prism-skin-swatch.is-active {
    border-color: #ffffff;
    transform: scale(1.1);
}

.white-content .prism-skin-swatch {
    box-shadow: 0 2px 6px rgba(29, 37, 59, 0.2);
}

.white-content .prism-skin-swatch.is-active {
    border-color: #1d253b;
}

/* The default's swatch is a gradient, not a flat hue. Its accent is #1d8cf8 —
   identical to `blue` directly below — so a flat swatch would render two
   indistinguishable dots side by side, one of them the product default.

   Literal, like the other nine, and NOT var(--prism-brand), however tempting
   that looks. Every swatch has to show the colour you would get by clicking
   it, which is a different question from what the page is currently painted
   in. A var() here resolves against the ACTIVE skin, so on green this dot
   would be a green-to-teal gradient advertising a blue-to-magenta skin. Same
   reason the whole block is literals. */
.prism-skin-swatch[data-skin="prism"]  { background: linear-gradient(135deg, #1d8cf8 0%, #8b5cf6 52%, #e14eca 100%); }
.prism-skin-swatch[data-skin="blue"]   { background: linear-gradient(135deg, #1d8cf8 0%, #3358f4 100%); }
.prism-skin-swatch[data-skin="purple"] { background: #ba54f5; }
.prism-skin-swatch[data-skin="green"]  { background: #00bf9a; }
.prism-skin-swatch[data-skin="orange"] { background: #f5703a; }
.prism-skin-swatch[data-skin="red"]    { background: #f5365c; }
.prism-skin-swatch[data-skin="pink"]   { background: #ec407a; }
.prism-skin-swatch[data-skin="indigo"] { background: #5e72e4; }
.prism-skin-swatch[data-skin="teal"]   { background: #11cdef; }
.prism-skin-swatch[data-skin="amber"]  { background: #f59e0b; }

/* Appearance rows inside the navbar profile dropdown — non-link list items
   need their own padding (the .dropdown-item class supplies it elsewhere). */
.prism-appearance-item {
    padding: 8px 16px;
}

.prism-appearance-item .prism-toggle {
    margin: 0;
}

/* ===== TAG FILTER STRIP ===== */
/* Used in: transactions.html, asset-transactions.html — click-to-toggle colored
   tag chips that filter the transaction list (OR semantics). Each chip sets
   its own --tag-color inline; the active state fills with that color. */
.tag-filter-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 10px 0;
}
.tag-filter-label {
    font-size: 0.7em;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--prism-text-muted);
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.tag-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    border-radius: 16px;background: var(--prism-surface-2);border: 1px solid var(--prism-border);color: var(--prism-text-2);
    font-size: 0.8em;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.tag-filter-chip:hover { border-color: var(--tag-color, var(--prism-accent)); }
.tag-filter-chip.active {
    background: var(--tag-color, var(--prism-accent));
    border-color: var(--tag-color, var(--prism-accent));
    color: #fff;
}
.tag-filter-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}
.tag-filter-chip.active .tag-filter-dot { background: #fff !important; }

.white-content .tag-filter-label { color: var(--prism-text-muted); }

/* ===== CHART TAG OMIT CONTROL ===== */
/* Used in: asset-transactions.html, dashboard.html, reports.html (via macros/chart_tag_omit.html)
   — a dropdown to exclude tagged transactions from the cashflow chart. */
.chart-tag-omit { position: relative; display: inline-block; }
/* Trigger button sits alongside the ownership toggle / Scope pills, which set
   line-height:1. prism-btn-* don't, so the button rendered ~7px taller. Match
   those pills exactly (padding/border/radius/font already align via btn-sm). */
.chart-tag-omit-btn.prism-btn-sm {
    line-height: 1;
    font-weight: 600;
    padding: 8px 20px;
}
.chart-tag-omit-menu {
    display: none;
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    z-index: 1000;
    min-width: 220px;
    max-height: 300px;
    overflow: auto;
    padding: 10px;
    text-align: left;background: var(--prism-surface-raised);border: 1px solid var(--prism-border);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);

    overscroll-behavior: contain;}
.chart-tag-omit-menu.open { display: block; }
.chart-tag-omit-title {
    font-size: 0.72em;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #8a93a6;
    margin-bottom: 8px;
    font-weight: 600;
}
.chart-tag-omit-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 2px;
    cursor: pointer;
    font-size: 0.85em;
    color: #d3d9e3;
}
.chart-tag-omit-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }
.chart-tag-omit-actions {
    display: flex;
    gap: 8px;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}
/* Compact, balanced Apply/Clear buttons — the default prism-btn-sm (8px 20px)
   is oversized for this dropdown. Used by the chart omit-tags + bulk-tag menus. */
.chart-tag-omit-actions .prism-btn-sm {
    flex: 1;
    min-width: 0;
    padding: 5px 12px;
    font-size: 0.72rem;
    letter-spacing: 0.3px;
}
.white-content .chart-tag-omit-menu {
    box-shadow: 0 8px 24px rgba(29, 37, 59, 0.18);
}
.white-content .chart-tag-omit-row { color: #525f7f; }
.white-content .chart-tag-omit-title { color: var(--prism-text-muted); }

/* Variant: dropdown opens UPWARD (used inside the bottom bulk-action overlay). */
.chart-tag-omit-menu--up {
    top: auto;
    bottom: calc(100% + 8px);
}

/* ===== BULK ACTION OVERLAY BAR ===== */
/* Used in: asset-transactions.html, transactions.html — a slim floating pill bar
   that appears at the bottom-center when transaction rows are selected. */
.bulk-overlay-bar {
    display: none;
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%) translateY(12px);
    z-index: 1050;
    align-items: center;
    gap: 14px;
    padding: 8px 10px 8px 18px;
    border-radius: 999px;background: var(--prism-surface-raised);border: 1px solid var(--prism-border);
    box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity 0.18s ease, transform 0.18s ease;
    max-width: calc(100vw - 32px);
    flex-wrap: wrap;
}
.bulk-overlay-bar.is-visible {
    display: flex;
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
.bulk-overlay-count {
    font-weight: 600;color: var(--prism-text);
    white-space: nowrap;
    font-size: 0.9em;
}
.bulk-overlay-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.bulk-overlay-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: none;
    border-radius: 50%;background: var(--prism-surface-hover);color: var(--prism-text-2);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.bulk-overlay-close:hover {background: var(--prism-surface-hover);color: var(--prism-text); }
.white-content .bulk-overlay-bar {
    box-shadow: 0 10px 34px rgba(29, 37, 59, 0.22);
}

/* ===== ROW "HAS TAGS" INDICATOR ===== */
/* Used in: asset-transactions.html, transactions.html — small colored dots on a
   transaction row showing it carries tags (dot color = the tag's color). */
.tx-row-tag-dots {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-left: 6px;
    vertical-align: middle;
}
.tx-row-tag-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex-shrink: 0;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
}
.white-content .tx-row-tag-dot {
    box-shadow: 0 0 0 1px rgba(29, 37, 59, 0.18);
}

/* ===== TRANSACTION ROW HOVER PREVIEW CARD =====
   Used in: asset-transactions.html, transactions.html (mounted by
   tx_hover_card.js). A single floating popover reused across all rows —
   surfaces everything (note, tags, id, account, category, status) that was
   previously buried behind per-icon tooltips. Read-only: pointer-events:none
   so it never blocks the row click that opens the full sidebar. */
#tx-hover-card {
    position: fixed;
    z-index: 10600;
    top: 0;
    left: 0;
    width: 340px;
    max-width: calc(100vw - 24px);
    padding: 14px 16px;
    border-radius: 12px;
    background: #23263a;border: 1px solid var(--prism-accent-border);
    box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45),
                0 0 0 1px rgba(255, 255, 255, 0.03) inset;color: var(--prism-text);
    font-size: 0.85em;
    line-height: 1.45;
    pointer-events: none;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.13s ease, transform 0.13s ease;
    display: none;
}
#tx-hover-card.is-visible {
    display: block;
    opacity: 1;
    transform: translateY(0);
}
.white-content #tx-hover-card {
    background: var(--prism-surface-raised);
    box-shadow: 0 14px 40px rgba(29, 37, 59, 0.18);
}

.tx-hover-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}
.tx-hover-merchant {
    font-weight: 600;
    font-size: 1.05em;
    word-break: break-word;
}
.tx-hover-amount {
    font-weight: 700;
    white-space: nowrap;
    flex-shrink: 0;
}
.tx-hover-amount.income {color: var(--prism-success); }
.tx-hover-amount.expense { color: var(--prism-danger); }

.tx-hover-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    font-size: 0.92em;
    color: rgba(255, 255, 255, 0.6);
    margin-bottom: 8px;
}
.white-content .tx-hover-meta { color: var(--prism-text-muted); }
.tx-hover-meta .tx-hover-sep { opacity: 0.4; }

/* Small labelled rows (Category / Account / Note / Tags) */
.tx-hover-field {
    display: flex;
    gap: 8px;
    padding: 3px 0;border-top: 1px solid var(--prism-border-faint);
}

.tx-hover-field-label {
    flex: 0 0 62px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 0.72em;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.4);
    padding-top: 2px;
}
.white-content .tx-hover-field-label { color: var(--prism-text-muted); }
.tx-hover-field-value {
    flex: 1 1 auto;
    min-width: 0;
    word-break: break-word;
}
.tx-hover-note { font-style: italic; }

/* Cloned category pills inside the card — let the parent➜child pair wrap AND
   let a long pill's text wrap inside itself so it never spills past the card
   edge (base .cat-badge is white-space:nowrap). */
.tx-hover-cat {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-width: 0;
    max-width: 100%;
}
.tx-hover-cat .category-badges-wrap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-width: 0;
    max-width: 100%;
}
.tx-hover-cat .cat-badge {
    white-space: normal;
    max-width: 100%;
    overflow-wrap: anywhere;
    text-align: left;
}
.tx-hover-cat img,
.tx-hover-cat .cat-icon-ph { flex-shrink: 0; vertical-align: middle; }

.tx-hover-status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}
.tx-hover-status::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    display: inline-block;
}
.tx-hover-status.reviewed { color: #2dce89; }
.tx-hover-status.unreviewed { color: var(--prism-danger); }
.tx-hover-status.pending { color: #f59e0b; }

.tx-hover-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
}
.tx-hover-tag {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px;
    border-radius: 12px;
    font-size: 0.85em;
    background: rgba(var(--prism-accent-rgb), 0.12);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.3);
}

.tx-hover-id {
    margin-top: 8px;
    padding-top: 6px;border-top: 1px solid var(--prism-border-faint);
    font-family: 'SFMono-Regular', Menlo, Consolas, monospace;
    font-size: 0.78em;
    color: rgba(255, 255, 255, 0.38);
    word-break: break-all;
}
.white-content .tx-hover-id {
    color: var(--prism-text-muted);
}

/* ===== MODERN COLOR PICKER (HSV) ===== */
/* Used in: categories.html, tags.html — the interactive custom color picker
   mounted by initializeModernColorPicker() in right_sidebar.js. Moved here from
   categories.html so any page can reuse it. */
.modern-color-picker {background: var(--prism-surface-2);border: 1px solid var(--prism-border);
    border-radius: 12px;
    padding: 16px;
    margin-top: 12px;
}

.color-picker-main {
    width: 100%;
    height: 200px;
    border-radius: 8px;
    cursor: crosshair;
    position: relative;
    margin-bottom: 12px;border: 2px solid var(--prism-border);
    overflow: hidden;
}

.color-picker-selector {
    position: absolute;
    width: 16px;
    height: 16px;
    border: 2px solid #fff;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.color-picker-hue-slider {
    width: 100%;
    height: 20px;
    border-radius: 10px;
    cursor: pointer;
    margin-bottom: 12px;border: 2px solid var(--prism-border);
    position: relative;
    background: linear-gradient(to right,
        #ff0000 0%,
        #ff7f00 14.3%,
        #ffff00 28.6%,
        #00ff00 42.9%,
        #00ffff 57.2%,
        #0000ff 71.5%,
        #7f00ff 85.8%,
        #ff00ff 100%);
}

.color-picker-hue-thumb {
    position: absolute;
    width: 20px;
    height: 20px;
    border: 2px solid #fff;
    border-radius: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
    background: transparent;
}
.color-picker-output {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px;background: var(--prism-surface-2);
    border-radius: 8px;
}

.color-picker-swatch {
    width: 40px;
    height: 40px;
    border-radius: 8px;border: 2px solid var(--prism-border-strong);
    flex-shrink: 0;
}

.color-picker-input-group {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 8px;
}
.color-picker-input {
    flex: 1;
    padding: 8px 12px;border: 2px solid var(--prism-accent-border);background: var(--prism-surface-2);color: var(--prism-text);
    border-radius: 6px;
    font-family: monospace;
    font-size: 0.9em;
}

.color-picker-input:focus {
    outline: none;
    border-color: var(--prism-accent);
    background: rgba(var(--prism-accent-rgb), 0.1);
}
.white-content .color-picker-input:focus {
    background: var(--prism-surface-raised);
}
.color-picker-copy-btn {
    padding: 8px 12px;
    background: rgba(var(--prism-accent-rgb), 0.2);
    border: 2px solid rgba(var(--prism-accent-rgb), 0.3);
    color: var(--prism-accent-text);
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    font-size: 0.9em;
    display: flex;
    align-items: center;
    gap: 6px;
}
.color-picker-copy-btn:hover {
    background: rgba(var(--prism-accent-rgb), 0.3);
    border-color: rgba(var(--prism-accent-rgb), 0.5);
}
.color-picker-format-select {
    padding: 8px 12px;border: 2px solid var(--prism-accent-border);background: var(--prism-surface-2);color: var(--prism-text);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9em;
}

.color-picker-format-select:focus {
    outline: none;
    border-color: var(--prism-accent);
}

/* ============================================================================
   SCROLL PROGRESS BAR
   Used in: layouts/base.html (every app page) and home.html (landing). ONE
   implementation, CSS and JS both — the landing page uses this class and loads
   prism-scroll-progress.js rather than keeping copies, so the two cannot drift
   on anything, not just on the spectrum.

   It rides the BOTTOM edge of the fixed navbar, not the top of the viewport.
   At top:0 it was a 4px rule against the lightest surface in light mode, at
   the extreme edge of the screen where the browser's own chrome competes with
   it, and it read as page trim rather than as part of the app. On the navbar's
   bottom edge it is a seam between two grounds, in the band the eye already
   goes to, and it takes over the job the 1px accent hairline used to do.

   It is still `fixed`, not a child of the navbar: an absolutely positioned
   child resolves against the PADDING box, and the navbar carries 15px of
   horizontal padding (0.9375rem from black-dashboard) that grows to
   `--rsb-w + 30px` on the right when the drawer opens. A child would be inset
   by that much and would jump when the drawer moved. Fixed keeps the bar on
   the viewport, and --prism-progress-top carries the navbar's measured bottom
   edge from prism-scroll-progress.js, which is also what makes it follow the
   mobile navbar as .navbar-collapse expands.

   z-index 1055 puts it over the navbar (1050) and the right sidebar (1051)
   while staying under Bootstrap's popover (1060) and tooltip (1070). It also
   clears the landing page's own nav, which is 1000.

   Two layers: the element is the full-width track, and ::after is the fill.
   The element cannot be both, because a single element sized to the scroll
   percentage has no way to draw the part not yet scrolled. That split is also
   why the fill width arrives as a custom property rather than as
   `style.width` — the JS is setting the pseudo-element's width, which is not
   addressable from script any other way.

   3px: past ~5px it stops reading as an edge and starts reading as a band.
   ============================================================================ */
.prism-scroll-progress {
    /* Both declared here rather than in the token blocks: they are per-element
       state written by prism-scroll-progress.js, not theme values, and the
       light/dark parity checks would demand a second definition of each that
       neither has any use for. The 0 fallback for --prism-progress-top puts
       the bar at the top of the viewport, which is where it belongs on a page
       with no navbar and is also what it does for the frame before the JS
       first measures. */
    --prism-progress-fill: 0%;
    --prism-progress-top: 0px;
    position: fixed;
    top: var(--prism-progress-top);
    left: 0;
    right: 0;
    height: 3px;
    background: var(--prism-progress-track);
    z-index: 1055;
    pointer-events: none;
    /* Matched to .right-sidebar's own `right` transition, because the inset
       below tracks the drawer's edge and the two classes are toggled in the
       same tick (right_sidebar.js). Snapping while the drawer glides breaks the
       alignment in both directions, and asymmetrically: on open the rail just
       vanishes from a strip the drawer has not reached yet, but on close the
       bar returns to full width instantly and spends the whole 0.4s painted
       over the drawer, which is the exact artefact the inset exists to prevent
       — the bar outranks the drawer, 1055 vs 1051.

       Same duration and easing is not approximate here, it is exact: the
       drawer's left edge and the bar's right edge are both (viewport −
       --rsb-w) offset by the same animated quantity, at every width and at
       both breakpoints, so identical timing glues them together.

       `right` ONLY. `top` is the measured navbar edge and must stay instant —
       transitioning it slides the bar down from the top of the viewport on
       every page load, before the first measurement lands. */
    transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Stop at the drawer instead of crossing it. The navbar does the same thing
   with padding-right; without this the bar cuts a 3px line across the drawer's
   header, which is the one surface in the app deliberately made of a different
   material. The fill stays proportional — it is a percentage of whatever width
   the track has.

   var(--rsb-w) and never a literal. The token is redefined to 100% at <=768px,
   where the drawer goes full-screen, so this collapses the bar to zero width
   and it disappears behind the drawer exactly as it should — the page under a
   full-screen overlay has no scroll position worth reporting. Pinning this to
   the desktop width instead left a stub of the rail painted over the drawer on
   every phone and tablet. Zero WIDTH rather than `display: none` on purpose:
   the bar keeps its 3px height, and prism-scroll-progress.js measures that
   height to place itself under the navbar. A display:none bar measures 0 and
   would come back 3px low when the drawer closed. */
body.right-sidebar-open .prism-scroll-progress {
    right: var(--rsb-w);
}

.prism-scroll-progress::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--prism-progress-fill);
    /* The gradient compresses into the fill rather than scrolling through a
       full-width one, so every scroll position shows all seven stops. That is
       the established look on the landing page; keep the two the same. */
    background: var(--prism-spectrum);
    box-shadow: var(--prism-progress-shadow);
    transition: width .1s linear;
}

@media (prefers-reduced-motion: reduce) {
    /* The fill only. The element's `right` transition is deliberately NOT
       killed here: it is not motion of its own, it is the bar keeping its edge
       on a drawer that animates regardless of this preference — nothing in
       either stylesheet reduces .right-sidebar's 0.4s slide. Cutting the bar's
       half of a matched pair would hand reduced-motion users the overlap the
       transition exists to prevent. If the drawer is ever made
       motion-sensitive, this has to change with it, in the same commit. */
    .prism-scroll-progress::after {
        transition: none;
    }
}

/* ============================================================================
   PRISM AMBIENT BACKGROUND
   Brings the /home landing page's layered "prism light" backdrop into the app.

   Type is deliberately NOT shared: the app stays on Poppins (loaded in
   layouts/base.html) and only the landing page uses Space Grotesk / Plus
   Jakarta Sans, which it loads itself as a standalone template.
   ========================================================================= */
body {
    /* Declared on body, not :root — the mesh references the accent triplet and
       the skins set that on body[data-skin]. See DERIVED ACCENT TOKENS above.

       Base page colour, painted by the backdrop below.

       This is the landing page's --px-bg, NOT black-dashboard's #1e1e2f. The
       two are far apart — measured bare backdrop was rgb(10,11,18) on /home vs
       rgb(28,28,44) in the app, ~3x the luminance — and because the mesh tints
       composite *over* the base, a lighter base washes them out too. Matching
       the base is what makes the app read like the landing page; the mesh
       alphas below are the landing page's as well. */
    --prism-bg-base: #0a0b12;
    /* Ambient mesh tints, keyed off the active skin so the backdrop follows
       whichever accent the user picked rather than hardcoding blue/pink. */
    --prism-bg-mesh:
        radial-gradient(60% 55% at 18% 12%, rgba(var(--prism-accent-rgb), .20) 0%, transparent 60%),
        radial-gradient(55% 50% at 82% 8%, rgba(var(--prism-accent2-rgb), .16) 0%, transparent 55%),
        radial-gradient(70% 60% at 50% 100%, rgba(var(--prism-accent3-rgb), .16) 0%, transparent 60%);
    --prism-bg-grid: rgba(255, 255, 255, .028);
}

/* Light mode (body.white-content). Same mesh geometry as dark on purpose — the
   two modes should read as one design, not as a theme and its washed-out
   cousin. The ground is #dde1ea, not the old #f5f6fa (95% of white): near-white
   edge to edge was the whole reason light mode read as harsh.

   The alphas are the interesting part. An rgba accent over a LIGHT ground
   subtracts light where the same accent over near-black adds it, so the tint
   both darkens and desaturates as it strengthens — push it far enough and the
   page reads dirty rather than branded. These sit at roughly 2.4x the first
   pass, which is where the blue and purple actually become visible, and the
   ground is lifted four points to pay for the darkening so the page does not
   get heavier overall.

   The ceiling is contrast, not taste. Where the blue and pink radials overlap
   (both are anchored top) the composited ground is ~#c9c9e8, and
   --prism-text-2 lands at 4.71:1 there. Raising these further walks that under
   WCAG AA, which is why .prism-section-subtitle now reads --prism-text-2 and
   not --prism-text-muted. tests/test_light_mode_css.py measures the composite,
   not the flat base, so this cannot drift silently. */
body.white-content {
    --prism-bg-base: #dde1ea;
    --prism-bg-mesh:
        radial-gradient(60% 55% at 18% 12%, rgba(var(--prism-accent-rgb), .12) 0%, transparent 60%),
        radial-gradient(55% 50% at 82% 8%, rgba(var(--prism-accent2-rgb), .102) 0%, transparent 55%),
        radial-gradient(70% 60% at 50% 100%, rgba(var(--prism-accent3-rgb), .102) 0%, transparent 60%);
    --prism-bg-grid: rgba(22, 27, 41, .03);
}

/* Ambient backdrop. Fixed, behind everything, so it parallaxes under the
   scrolling content exactly like the landing page. It paints the base colour
   too, so light/dark is driven entirely by --prism-bg-base and nothing needs
   to select <html> from a class that lives on <body>. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -2;
    pointer-events: none;
    background: var(--prism-bg-mesh), var(--prism-bg-base);
}

/* Fine grid, faded out toward the edges. Purely decorative — masked so it
   never competes with dense tables. */
body::after {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image:
        linear-gradient(var(--prism-bg-grid) 1px, transparent 1px),
        linear-gradient(90deg, var(--prism-bg-grid) 1px, transparent 1px);
    background-size: 46px 46px;
    -webkit-mask-image: radial-gradient(circle at 50% 30%, #000 0%, transparent 78%);
    mask-image: radial-gradient(circle at 50% 30%, #000 0%, transparent 78%);
}

/* The pseudo-elements sit at negative z-index, so they only show if nothing in
   front of them paints an opaque colour.

   `.main-panel` is the one that matters: black-dashboard gives it
   `background: linear-gradient(#1e1e2f,#1e1e24)` (and `#f5f6fa` in light mode)
   and it spans the whole content area at `min-height:100vh`, so it covered the
   backdrop everywhere except the sidebar gutter. Clearing it is what makes the
   mesh actually visible — the base colour it used to supply now comes from
   --prism-bg-base on body::before, so the page never goes bare.

   `background: none` (not `background-color`) because those are gradients,
   which live in background-image. */
body,
body.white-content,
body .wrapper {
    background-color: transparent;
}
body .main-panel,
body.white-content .main-panel {
    background: none;
}

/* The vendor also gives .main-panel `border-top: 2px solid #e14eca`
   (black-dashboard.css:17818) — a hardcoded pink hairline that follows no skin
   and no mode. It is very nearly invisible in practice, since the fixed navbar
   covers the top of the panel, which is exactly why it survived every previous
   pass. Pointed at the neutral border token rather than deleted: the 2px is
   real layout, and removing it would shift the content up by two pixels on
   every page to fix something nobody can see.

   One selector, not the two above: --prism-border already flips, so a
   .white-content twin would be a colour-only restatement — which is the exact
   thing the ratchet exists to reject, and it did reject it. */
body .main-panel {
    border-top-color: var(--prism-border);
}

/* ===== ASSET PERFORMANCE ===== */
/* Used in: asset-performance.html
   Per-asset accordion, score ring, focus-area callouts and chart shells.
   Everything else on that page reuses stats-hero / stats-metric-card /
   stats-section-divider / ncf-filter / prism-card. */

/* --- Toolbar above the asset list --- */
.perf-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 18px;
}

.perf-toolbar-group {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

/* `hidden` is UA-level and loses to the display above, so without this the JS
   sets the attribute and the control stays on screen — which is how the
   compare-against select sat beside a table it cannot change. */
.perf-toolbar-group[hidden] {
    display: none;
}

.perf-tool-btn {
    background: transparent;border: 1px solid var(--prism-accent-border);color: var(--prism-text-muted);
    border-radius: 8px;
    padding: 5px 12px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    transition: all 0.2s ease;
}

.perf-tool-btn:hover {
    background: rgba(var(--prism-accent-rgb), 0.14);
    border-color: rgba(var(--prism-accent-rgb), 0.5);color: var(--prism-text);
}

.perf-tool-label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.09em;color: var(--prism-text-muted);
}

.perf-tool-select {background: var(--prism-tint-3);border: 1px solid var(--prism-accent-border);color: var(--prism-text);
    border-radius: 8px;
    padding: 5px 10px;
    font-size: 0.78rem;
    font-weight: 600;
    /* Same reason as .prism-select: the native popup only follows the
       scheme token, never the option rules. */
    color-scheme: var(--prism-color-scheme, dark);
}

.perf-tool-select option {background: var(--prism-surface-raised);color: var(--prism-text);
}

/* --- Accordion card --- */
.perf-asset {
    border: 1px solid rgba(var(--prism-accent-rgb), 0.18);
    border-radius: 12px;
    background: linear-gradient(180deg, rgba(var(--prism-accent-rgb), 0.05) 0%, rgba(7, 17, 37, 0.28) 100%);
    margin-bottom: 16px;
    overflow: hidden;
    transition: border-color 0.25s ease, box-shadow 0.25s ease;
}

.perf-asset:hover {border-color: var(--prism-accent-border);
    box-shadow: 0 10px 26px rgba(3, 11, 28, 0.28);
}

.perf-asset.is-open {
    border-color: rgba(var(--prism-accent-rgb), 0.45);
}

.white-content .perf-asset {
    background: var(--prism-surface-raised);
    border-color: rgba(29, 55, 110, 0.12);
    box-shadow: 0 6px 18px rgba(29, 37, 59, 0.05);
}

.white-content .perf-asset:hover {
    box-shadow: 0 10px 24px rgba(29, 37, 59, 0.1);
}

/* --- Accordion header (a real <button>) --- */
.perf-asset-head {
    display: flex;
    align-items: center;
    gap: 16px;
    width: 100%;
    padding: 16px 20px;
    background: transparent;
    border: 0;
    text-align: left;
    color: inherit;
    transition: background 0.2s ease;
}

.perf-asset-head:hover {
    background: rgba(var(--prism-accent-rgb), 0.07);
}

.perf-asset-head:focus-visible {
    outline: 2px solid rgba(var(--prism-accent-rgb), 0.6);
    outline-offset: -2px;
}

.perf-asset-head-main {
    min-width: 0;
    flex: 1 1 auto;
}

.perf-asset-title {
    display: block;
    font-size: 1.02rem;
    font-weight: 700;color: var(--prism-text);
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.perf-asset-addr {
    display: block;
    font-size: 0.78rem;color: var(--prism-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Headline metric chips in the collapsed header */
.perf-asset-chips {
    display: flex;
    align-items: center;
    gap: 20px;
    flex: 0 0 auto;
}

.perf-chip-label {
    display: block;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.09em;color: var(--prism-text-muted);
    margin-bottom: 2px;
}

.perf-chip-value {
    display: block;
    font-size: 0.95rem;
    font-weight: 700;color: var(--prism-text);
    line-height: 1.1;
}

.perf-asset-chevron {
    flex: 0 0 auto;
    color: var(--prism-accent-text);
    font-size: 0.8rem;
    transition: transform 0.25s ease;
}

.perf-asset-head[aria-expanded="true"] .perf-asset-chevron {
    transform: rotate(180deg);
}

/* --- Score ring --- */
.perf-score {
    position: relative;
    width: 56px;
    height: 56px;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
}

/* Masked conic gradient: a true ring with a transparent centre, so the card
   background shows through and the hole never has to match a surface colour. */
.perf-score-ring {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: conic-gradient(var(--perf-ring, var(--prism-accent)) calc(var(--perf-score, 0) * 3.6deg),
                               rgba(255, 255, 255, 0.09) 0);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 5px));
            mask: radial-gradient(farthest-side, transparent calc(100% - 5px), #000 calc(100% - 5px));
}

.white-content .perf-score-ring {
    background: conic-gradient(var(--perf-ring, var(--prism-accent)) calc(var(--perf-score, 0) * 3.6deg),
                               rgba(29, 37, 59, 0.1) 0);
}

.perf-score-val {
    position: relative;
    font-size: 1rem;
    font-weight: 800;color: var(--prism-text);
    letter-spacing: -0.5px;
}

/* --- Score band badges (replaces the old .score-badge/.score-* set) --- */
.perf-badge-excellent,
.perf-badge-good,
.perf-badge-average,
.perf-badge-poor {
    display: inline-block;
    padding: 4px 11px;
    border-radius: 15px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}

.perf-badge-excellent {background: var(--prism-success-wash);
    color: var(--prism-success);
    border: 1px solid rgba(0, 242, 195, 0.35);
}

.perf-badge-good {
    background: rgba(var(--prism-accent-rgb), 0.16);
    color: var(--prism-accent-text);
    border: 1px solid rgba(var(--prism-accent-rgb), 0.35);
}

.perf-badge-average {background: var(--prism-warning-wash);
    color: var(--prism-warning);border: 1px solid var(--prism-warning-border);
}

.perf-badge-poor {background: var(--prism-danger-wash);
    color: var(--prism-danger);
    border: 1px solid rgba(253, 93, 147, 0.35);
}

.white-content .perf-badge-excellent {
    color: #00926f;
    border-color: rgba(0, 191, 154, 0.3);
}

.white-content .perf-badge-average {
    color: #c4501f;
}

.white-content .perf-badge-poor {
    color: #c9184a;
    border-color: rgba(253, 93, 147, 0.3);
}

/* --- Expanded body --- */
.perf-asset-body {
    padding: 4px 20px 22px;
    border-top: 1px solid rgba(var(--prism-accent-rgb), 0.14);
}

.white-content .perf-asset-body {
    border-top-color: rgba(29, 55, 110, 0.1);
}

.perf-asset-body[hidden] {
    display: none;
}

/* Metric tiles inside an asset. Auto-fill grid rather than Bootstrap columns —
   the tile count varies per section. Tiles themselves reuse stats-metric-card. */
.perf-metric-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 14px;
    margin-bottom: 26px;
}

.perf-metric-sub {
    font-size: 0.72rem;color: var(--prism-text-muted);
    margin-top: 6px;
}

/* --- Focus-area callouts --- */
.perf-focus {
    border-radius: 10px;
    border-left: 3px solid;
    padding: 12px 16px;
    margin-bottom: 10px;
}

.perf-focus--high {
    background: rgba(253, 93, 147, 0.1);
    border-left-color: #fd5d93;
}

.perf-focus--medium {
    background: rgba(255, 141, 114, 0.1);
    border-left-color: #ff8d72;
}

.perf-focus-head {
    font-size: 0.88rem;
    font-weight: 600;color: var(--prism-text);
    margin-bottom: 3px;
}

.perf-focus-area {
    font-weight: 700;
}

.perf-focus--high .perf-focus-area {
    color: var(--prism-danger);
}

.perf-focus--medium .perf-focus-area {
    color: var(--prism-warning);
}

.perf-focus-fix {
    font-size: 0.79rem;color: var(--prism-text-muted);
}

/* --- Chart shells --- */
.perf-chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 16px;
    margin-bottom: 26px;
}

/* Another card-shaped surface that hardcoded a dark literal and needed a
   light-mode rule to undo it. Both go. */
.perf-chart-card {
    border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-panel);
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    padding: 16px 14px 10px;
}

.perf-chart-title {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--prism-accent-text);
    margin-bottom: 10px;
    padding-left: 4px;
}

.perf-chart-box {
    width: 100%;
    min-height: 320px;
}

/* --- Risk table ---
   Purpose-built rather than reusing .report-table, whose sticky-header /
   section-row / subtotal machinery is built for the income statement. */
.perf-risk-table {
    width: 100%;
    border-collapse: collapse;
}

.perf-risk-table th {
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;color: var(--prism-text-muted);
    padding: 8px 12px;border-bottom: 1px solid var(--prism-accent-border);
    text-align: left;
}

.perf-risk-table td {
    padding: 10px 12px;
    font-size: 0.86rem;
    color: #d4dbe8;border-bottom: 1px solid var(--prism-border-faint);
}

.perf-risk-table tr:last-child td {
    border-bottom: 0;
}

.white-content .perf-risk-table td {
    color: #525f7f;
}

@media (max-width: 991px) {
    .perf-asset-chips {
        display: none;
    }
}

@media (max-width: 575px) {
    .perf-asset-head {
        padding: 14px;
        gap: 12px;
    }

    .perf-asset-body {
        padding: 4px 14px 18px;
    }

    .perf-chart-grid {
        grid-template-columns: 1fr;
    }
}

/* ============================================================================
   VENDOR SURFACE DECOUPLING
   Replaces the 66 `.white-content` rules that used to live in
   black-dashboard.css. Those are now deleted from the vendor sheet, so light
   mode no longer depends on it at all — when black-dashboard.css eventually
   goes, that becomes a layout problem rather than a re-theming one.

   Every rule here is MODE-AGNOSTIC: it replaces the vendor's dark rule AND its
   light override with one token-driven rule. Selectors are `body`-prefixed to
   clear the vendor's element-level specificity (its `h1..h6` is 0-0-1, this is
   0-0-2) and prism-common.css loads last, so ties go to us.
   ============================================================================ */

/* ---- Document text ---- */
body h1, body h2, body h3, body h4, body h5, body h6 {
    color: var(--prism-heading);
}

body p,
body ol li,
body ul li,
body pre {
    color: var(--prism-body);
}

/* Text INSIDE a card needs its own, higher-specificity rule. The vendor sets
   `.card h1…h6, .card p { color: rgba(255,255,255,0.8) }` at
   black-dashboard.css:16462 — specificity 0-1-1, which beats the 0-0-2 of
   `body h1` above no matter what order the sheets load in. Its light
   counterpart used to neutralise it; removing that left near-white text
   sitting on light cards at 1.17:1. The rendered audit caught it across
   nine pages. Match the vendor's specificity and let load order decide. */
body .card h1, body .card h2, body .card h3,
body .card h4, body .card h5, body .card h6,
body .modal h1, body .modal h2, body .modal h3,
body .modal h4, body .modal h5, body .modal h6 {
    color: var(--prism-heading);
}

body .card p,
body .card ol li,
body .card ul li,
body .modal .modal-body p {
    color: var(--prism-body);
}

body .card .card-header,
body .card .card-title,
body .card .card-category,
body .card .card-description {
    color: var(--prism-text);
}

/* The sidebar is a light surface in light mode (see .sidebar.prism-sidebar
   above), so it must NOT inherit the near-white heading colour there — this is
   what black-dashboard's `.white-content .sidebar p` rule was fighting. */
body .sidebar.prism-sidebar p,
body .sidebar.prism-sidebar .nav li > a p {
    color: var(--prism-text-2);
}

/* ---- Cards ----
   A Bootstrap card and a .prism-card sit next to each other on several pages
   (the profile page, billing, the admin views) and until now they were not the
   same object: neutralising the vendor's #ffffff left this one with NO surface
   at all — a shadow around a hole. It reads the same material as .prism-card,
   so the two are one component with two class names rather than two
   components. */
body .card:not(.card-white) {
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    border: 1px solid var(--prism-border);
    box-shadow: 0 1px 15px 0 var(--prism-shadow-soft);
}

body .card:not(.card-white) .card-header,
body .card:not(.card-white) .card-header a[data-toggle="collapse"],
body .card:not(.card-white) .card-header .card-title,
body .card:not(.card-white) .card-body .card-title,
body .card:not(.card-white) .card-body .card-category,
body .card:not(.card-white) .card-body .card-description {
    color: var(--prism-text);
}

body .card:not(.card-white) label:not(.btn) {
    color: var(--prism-text-2);
}

/* A card inside a card gets neither the shadow nor a second border — and now
   neither the second veil, which would otherwise stack to twice the fill. */
body .card:not(.card-white) .card {
    background: none;
    border: 0;
    box-shadow: none;
}

body .card:not(.card-white).card-plain {
    background: transparent;
    box-shadow: none;
}

body .card:not(.card-white).card-tasks .card-body i {
    color: var(--prism-text-muted);
}

body .card:not(.card-white).card-tasks .card-body i:hover {
    color: var(--prism-text);
}

/* ---- Forms ---- */

/* The vendor styles textarea.form-control for a look this app no longer has:
   an underline-only field, so `padding: 10px 10px 0 0` — zero LEFT padding —
   plus `max-height: 80px` and `resize: none` (black-dashboard.css:19087). The
   later pill restyle loses to it on specificity for padding while its
   `border-radius: 30px !important` wins, which produced the worst of both: a
   fully-bordered rounded box whose text sat flush against the left edge, with
   three declared rows clipped to 80px. Layout only — the colours already come
   from `body .form-control` below. */
body textarea.form-control {
    padding: 12px 15px;
    max-height: none;
    resize: vertical;
    line-height: 1.6;
}

body .form-control {
    color: var(--prism-text);
    border-color: var(--prism-border-strong);
}

body .form-control::placeholder {
    color: var(--prism-text-faint);
    opacity: 1;
}

/* The `color` here looks redundant next to `body .form-control` above and is
   not: the vendor restates it on :focus (black-dashboard.css:2102,
   `rgba(255,255,255,.8)`), and `.form-control:focus` at (0,2,0) BEATS
   `body .form-control` at (0,1,1) — two classes outrank one class plus one
   element, whatever the source order. So without this declaration the token is
   in force at rest and abandoned the moment the field is focused.

   Invisible in dark mode, where --prism-text is #ffffff and the vendor's value
   is within a rounding error of it. In light mode the text turned white on a
   light card the instant you typed in it, and the caret went with it, since
   caret-color follows color. Every .form-control in the app, not just the two
   asset forms. Deleting this line reintroduces all of it. */
body .form-control:focus {
    color: var(--prism-text);
    border-color: var(--prism-accent);
}

/* Same shape as :focus above — the vendor sets `color: #7587b9` at (0,2,0)
   (black-dashboard.css:19066) and this rule previously answered with background
   and border only, so a disabled value rendered a fixed periwinkle in both
   modes. Live on the asset form in two places: ticking Cash purchase disables
   five money fields, and the edit page's asset_name is readonly.

   --prism-text-2 rather than a dim token: these are values the reader still has
   to read, and --prism-input-disabled already carries the disabled affordance. */
body .form-control[disabled],
body .form-control[readonly],
body fieldset[disabled] .form-control {
    color: var(--prism-text-2);
    background: var(--prism-input-disabled);
    border-color: var(--prism-border-strong);
}

body .input-group-prepend .input-group-text,
body .input-group-append .input-group-text {
    border-color: var(--prism-border-strong);
    color: var(--prism-text);
}

body .input-group-prepend .input-group-text { border-right: none; }
body .input-group-append  .input-group-text { border-left: none; }

body .form-group.no-border .form-control,
body .form-group.no-border .form-control + .input-group-prepend .input-group-text {
    background-color: var(--prism-input-bg);
}

body .form-group.no-border .form-control:focus,
body .form-group.no-border .form-control:active {
    background-color: var(--prism-surface-hover);
}

body .input-group[disabled] .input-group-prepend .input-group-text,
body .input-group[disabled] .input-group-append .input-group-text {
    background-color: var(--prism-input-disabled);
}

body .input-group-focus .input-group-prepend .input-group-text,
body .input-group-focus .input-group-append .input-group-text {
    border-color: var(--prism-accent2);
}

/* Validation states keep their semantic hues — those are meaning, not theme. */
body .has-danger .form-control,
body .has-danger .form-control:focus,
body .has-danger .input-group-prepend .input-group-text {
    border-color: var(--prism-danger);
}

body .has-success .form-control:focus {
    border-color: var(--prism-success);
}

/* ---- Buttons ---- */
body .btn-neutral.btn-link {
    color: var(--prism-text-muted);
}

body .btn.btn-link:hover,
body .btn.btn-link:active,
body .btn.btn-link:focus {
    color: var(--prism-text-muted) !important;
}

/* ---- Tables ---- */
body .table > tbody > tr > td {
    color: var(--prism-text-2) !important;
}

body .table > thead > tr > th,
body .table > tbody > tr > th,
body .table > tfoot > tr > th,
body .table > thead > tr > td,
body .table > tbody > tr > td,
body .table > tfoot > tr > td {
    border-color: var(--prism-border-strong);
    padding: 12px 7px;
    vertical-align: middle;
}

body .table > thead > tr > th,
body .table button.btn-neutral.btn-link {
    color: var(--prism-text-muted);
}

/* ---- Nav pills ---- */
body .nav-pills .nav-item .nav-link:not(.active) {
    background: var(--prism-surface-2);
    color: var(--prism-text);
}

body .nav-pills .nav-item .nav-link:not(.active):hover {
    background: var(--prism-surface-hover);
}

/* ---- Footer + progress ---- */
body .footer ul li a,
body .footer .copyright {
    color: var(--prism-text-2);
}

body .progress-container .progress {
    background: var(--prism-surface-2);
    box-shadow: 0 0 0 3px var(--prism-surface-2);
}

body .progress-container .progress .progress-value,
body .progress-container .progress-badge {
    color: var(--prism-text-2);
}

/* ---- Full-page (auth) layouts ---- */
body .full-page h1, body .full-page h2, body .full-page h3,
body .full-page h4, body .full-page h5, body .full-page h6 {
    color: var(--prism-heading);
}

body .full-page .description {
    color: var(--prism-text-muted);
}

/* ---- Transparent navbar ---- */
body .navbar.navbar-transparent .navbar-brand,
body .navbar.navbar-transparent .navbar-nav li a:not(.dropdown-item),
body .navbar.navbar-transparent .navbar-nav li a:not(.dropdown-item) i,
body .navbar.navbar-transparent .navbar-minimize button i,
body .navbar.navbar-transparent .search-bar.input-group i {
    color: var(--prism-text);
}

body .navbar.navbar-transparent .navbar-toggler-bar {
    background: var(--prism-text);
}

body .navbar.navbar-transparent .search-bar.input-group .form-control {
    color: var(--prism-text-2);
}

body .navbar.navbar-transparent .search-bar.input-group .form-control::placeholder {
    color: var(--prism-text-muted);
}

/* ---- Bootstrap semantic text utilities ----
   black-dashboard.css hardcodes these mode-invariantly with !important
   (.text-success is #00f2c3 at :12438). On a light card that green measures
   1.19:1 -- the income and net totals on the transaction list were legible
   only because you already knew what they said. Semantic meaning is constant
   across modes; the value it renders at is not. */
body .text-success   { color: var(--prism-success) !important; }
body .text-danger    { color: var(--prism-danger) !important; }
body .text-warning   { color: var(--prism-warning) !important; }
body .text-info      { color: var(--prism-accent-text) !important; }
body .text-primary   { color: var(--prism-accent2-text) !important; }
body .text-muted     { color: var(--prism-text-muted) !important; }
body .text-secondary { color: var(--prism-text-2) !important; }

/* ---- Filled semantic buttons and badges ----
   The vendor fills these with the DARK-mode semantic colour and puts white
   text on it (black-dashboard.css:2737 — .btn-success is #00f2c3). That is
   fine over a dark ground, but the fill does not change in light mode, so
   white-on-#00f2c3 renders at 1.45:1 — the label is essentially gone. The
   rendered audit found it on every page with a success or warning button.

   Pointing the FILL at the token fixes it from the other direction: the
   light-mode values are dark enough that white text lands at 5.8-6.2:1, and
   dark mode is unchanged because the tokens resolve to the same colours it
   already used. Specificity matches the vendor's `.btn-success` (0-1-0) and
   prism-common.css loads last. */
body .btn-success:not(.btn-link):not(.btn-simple),
body .btn.btn-fill.btn-success {
    background-color: var(--prism-success);
    border-color: var(--prism-success);
    color: var(--prism-on-semantic);
}

body .btn-warning:not(.btn-link):not(.btn-simple),
body .btn.btn-fill.btn-warning {
    background-color: var(--prism-warning);
    border-color: var(--prism-warning);
    color: var(--prism-on-semantic);
}

body .btn-danger:not(.btn-link):not(.btn-simple),
body .btn.btn-fill.btn-danger {
    background-color: var(--prism-danger);
    border-color: var(--prism-danger);
    color: var(--prism-on-semantic);
}

/* Same problem in badge form — the unreviewed-count pill and friends. */
body .badge-success  { background-color: var(--prism-success); color: var(--prism-on-semantic); }
body .badge-warning  { background-color: var(--prism-warning); color: var(--prism-on-semantic); }
body .badge-danger   { background-color: var(--prism-danger);  color: var(--prism-on-semantic); }
body .unreviewed-badge { background: var(--prism-warning); color: var(--prism-on-semantic); }

/* ---- Bootstrap ACCENT utilities: .btn-info / .btn-primary / .bg-* / .badge-* ----
   The block above fixed the semantic four. These are the other family, and they
   were missed for a subtler reason: they are not wrong in either mode, so no
   contrast test could see them. They are wrong in a way only the skin picker
   reveals — they hardcode the vendor's blue and pink and therefore follow NO
   skin at all. ~160 uses across 45 templates sat one class away from the token
   layer and never moved when a user picked green.

   `.text-info` / `.text-primary` were already pointed at tokens two rules up,
   which is what made this so easy to miss: half the family was done.

   Mapped onto the app's OWN pairs rather than the vendor's, so a Bootstrap
   button and a .prism-btn-* sitting side by side are the same colour:
     .btn-info    -> accent -> accent-light   (matches .prism-btn-info)
     .btn-primary -> accent2 -> accent3       (matches .prism-btn-primary)
   On the default skin that changes exactly one hue — the vendor's `#3358f4`
   middle stop becomes `--prism-accent-light` — and the vendor's own
   `background-size`/`position` are preserved so `.animation-on-hover` still
   sweeps.

   !important is not decoration here. The vendor's own hover/active rules carry
   it (black-dashboard.css:13125-13129 and 13375-13379), and !important beats
   specificity, so a merely more-specific selector loses. It is used ONLY where
   the vendor used it first. */
body .btn-info:not(.btn-link):not(.btn-simple) {
    background: var(--prism-accent);
    background-image: linear-gradient(to bottom left,
        var(--prism-accent), var(--prism-accent-light), var(--prism-accent));
    background-color: var(--prism-accent);
    border-color: var(--prism-accent);
}

body .btn-info:not(.btn-link):not(.btn-simple):hover,
body .btn-info:not(.btn-link):not(.btn-simple):focus,
body .btn-info:not(.btn-link):not(.btn-simple):active,
body .btn-info:not(.btn-link):not(.btn-simple).active {
    background-color: var(--prism-accent-light) !important;
    background-image: linear-gradient(to bottom left,
        var(--prism-accent), var(--prism-accent-light), var(--prism-accent)) !important;
    border-color: var(--prism-accent-light);
}

body .btn-primary:not(.btn-link):not(.btn-simple) {
    background: var(--prism-accent2);
    background-image: linear-gradient(to bottom left,
        var(--prism-accent2), var(--prism-accent3), var(--prism-accent2));
    background-color: var(--prism-accent2);
    border-color: var(--prism-accent2);
}

body .btn-primary:not(.btn-link):not(.btn-simple):hover,
body .btn-primary:not(.btn-link):not(.btn-simple):focus,
body .btn-primary:not(.btn-link):not(.btn-simple):active,
body .btn-primary:not(.btn-link):not(.btn-simple).active {
    background-color: var(--prism-accent3) !important;
    background-image: linear-gradient(to bottom left,
        var(--prism-accent2), var(--prism-accent3), var(--prism-accent2)) !important;
    border-color: var(--prism-accent3);
}

/* Outline variants read the TEXT token, not the raw accent: this colour lands
   on the page ground, where #1d8cf8 measures 2.81:1 in light mode. Filling on
   hover is the vendor's behaviour and is kept. */
body .btn-outline-info {
    color: var(--prism-accent-text);
    border-color: var(--prism-accent-border);
    background-color: transparent;
}

body .btn-outline-info:hover,
body .btn-outline-info:focus,
body .btn-outline-info:active {
    color: #ffffff;
    background-color: var(--prism-accent);
    border-color: var(--prism-accent);
}

/* The vendor marks these !important (black-dashboard.css:7229, 7262). */
body .bg-primary { background-color: var(--prism-accent2) !important; }
body .bg-info    { background-color: var(--prism-accent) !important; }

body .badge-primary { background-color: var(--prism-accent2); color: #ffffff; }
body .badge-info    { background-color: var(--prism-accent);  color: #ffffff; }

/* `.alert-info` was not merely unskinned, it was unreadable: the vendor sets
   `color: #1d8cf8` on `background-color: #419ef9` (black-dashboard.css:5867),
   which is blue text on blue at roughly 1.5:1, in BOTH modes. A light-mode-only
   patch for it already existed further up this file; making the base rule a
   token-driven wash fixes dark too and lets that override go. */
body .alert-info {
    background: var(--prism-tint-3);
    border-color: var(--prism-accent-border);
    color: var(--prism-text);
}

body .alert-info .prism-text-muted,
body .alert-info .alert-link {
    color: var(--prism-text-muted);
}

/* `.card-category` is the last of the vendor's dark card rules that outranked
   the generic body ramp (rgba(255,255,255,.6) at 1.13:1 on a light card). */
/* Also un-nested: the vendor's base `.card-category` (0-1-0,
   black-dashboard.css:16603) applies to instances that sit outside a
   .card-body, which the card-scoped rules below never reach. */
body .card-category,
body .stats {
    color: var(--prism-text-muted);
}

body .card .card-body .card-category,
body .card .card-header .card-category,
body .card .card-body .stats,
body .card p.card-category {
    color: var(--prism-text-muted);
}


/* ============================================================================
   ERROR PAGES
   Used in: layouts/error-base.html — page-401-custom.html, page-403.html,
   page-403-custom.html, page-404.html, page-500.html.

   All five render the same chrome, so it lives here rather than in a page
   <style> block: the pre-Prism versions were five copies of the same markup,
   and five copies is exactly why they all missed the last UI pass.

   The one memorable device is the hollow status numeral behind the copy —
   monumental but weightless, so it frames the message instead of competing
   with it. Everything else is quiet on purpose: a person reading this page is
   already having a bad time and wants the exit, not a composition.

   Mode-agnostic throughout. Every colour is a token, so light mode is the
   usual token flip with no `.white-content` override — see the token contract
   at the top of this file.
   ============================================================================ */

.error-page {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 62vh;
    text-align: center;
}

.error-stage {
    position: relative;
    width: 100%;
    max-width: 620px;
    padding: 10px 15px 40px;
    /* 4xx is something the request asked for, 5xx is something we broke. The
       template picks the class; everything tone-coloured reads these two. */
    --error-tone: var(--prism-warning);
    --error-tone-wash: var(--prism-warning-wash);
}

.error-stage.error-tone-danger {
    --error-tone: var(--prism-danger);
    --error-tone-wash: var(--prism-danger-wash);
}

/* Ambient bloom behind the numeral, so the stage reads as lit rather than as
   content stranded in the middle of an empty page. */
.error-stage::before {
    content: '';
    position: absolute;
    top: -6%;
    left: 50%;
    width: min(520px, 100%);
    height: 320px;
    transform: translateX(-50%);
    background: radial-gradient(ellipse at center,
                rgba(var(--prism-accent-rgb), 0.12) 0%,
                rgba(var(--prism-accent2-rgb), 0.06) 45%,
                rgba(var(--prism-accent-rgb), 0) 70%);
    pointer-events: none;
    z-index: 0;
}

.error-code {
    position: relative;
    z-index: 0;
    font-size: clamp(7rem, 28vw, 15rem);
    font-weight: 800;
    line-height: 0.85;
    letter-spacing: 0.02em;
    white-space: nowrap;
    user-select: none;
    pointer-events: none;
    /* Hollow, not filled: the copy sits ON the numeral, and a solid one would
       fight the headline for the eye instead of framing it. */
    background: linear-gradient(180deg, var(--prism-tint-4) 0%,
                rgba(var(--prism-accent-rgb), 0) 78%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    -webkit-text-stroke: 1px var(--prism-accent-border);
    /* The overlap is in `em` so it tracks the clamp() above. A px value
       un-overlaps at the small end and swallows the eyebrow at the large one. */
    margin: 0 0 -0.26em;
}

.error-eyebrow {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 16px;
    padding: 7px 18px;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--prism-text-2);
    background: var(--prism-tint-2);
    border: 1px solid var(--prism-border);
}

.error-status-dot {
    width: 8px;
    height: 8px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--error-tone);
    animation: errorStatusPulse 2.6s ease-out infinite;
}

.error-headline {
    position: relative;
    z-index: 1;
    font-size: clamp(1.9rem, 5vw, 2.6rem);
    font-weight: 800;
    line-height: 1.15;
    margin: 0 0 14px;
}

.error-message {
    position: relative;
    z-index: 1;
    max-width: 46ch;
    margin: 0 auto 32px;
    font-size: 1.05rem;
    font-weight: 300;
    line-height: 1.7;
    /* --prism-text-2, not --prism-text-muted, for the same reason as
       .prism-section-subtitle: this sits on the open page ground with the
       ambient mesh composited under it, where muted drops below AA. */
    color: var(--prism-text-2);
}

.error-actions {
    position: relative;
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    justify-content: center;
}

/* The prism buttons set `display: inline-block`, which outranks the UA sheet's
   [hidden] rule — without this the "Go back" button stays visible even when
   error-base.html decided there is nowhere to go back to. */
.error-actions [hidden] {
    display: none;
}

/* One orchestrated entrance rather than scattered micro-motion: the numeral
   settles, then the copy rises through it in reading order. */
.error-code,
.error-eyebrow,
.error-headline,
.error-message,
.error-actions {
    animation: errorRise 0.65s cubic-bezier(0.22, 0.9, 0.32, 1) backwards;
}

.error-code {
    animation-name: errorWatermarkSettle;
    animation-duration: 1.1s;
}

.error-eyebrow  { animation-delay: 0.08s; }
.error-headline { animation-delay: 0.16s; }
.error-message  { animation-delay: 0.24s; }
.error-actions  { animation-delay: 0.32s; }

@keyframes errorRise {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

@keyframes errorWatermarkSettle {
    from { opacity: 0; transform: scale(1.08); }
    to   { opacity: 1; transform: none; }
}

/* rgba(0,0,0,0) rather than `transparent` so the ring interpolates instead of
   fading through grey. */
@keyframes errorStatusPulse {
    0%   { box-shadow: 0 0 0 0 var(--error-tone-wash); }
    70%  { box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); }
    100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
    .error-code,
    .error-eyebrow,
    .error-headline,
    .error-message,
    .error-actions,
    .error-status-dot {
        animation: none;
    }
}

@media (max-width: 576px) {
    .error-page {
        min-height: 50vh;
    }

    .error-actions {
        flex-direction: column;
        align-items: stretch;
    }
}

/* ============================================================================
   IMAGE LIGHTBOX
   Used in: home.html (the landing page's product screenshots), driven by
   assets/js/prism-lightbox.js.

   It lives here rather than in home.html's <style> block because expanding an
   image is not a landing-page idea, and that block is already the largest
   single pool of styling outside the design system — see
   tests/test_page_local_css.py, which ratchets exactly that.

   IT IS A <dialog> OPENED WITH showModal(), and that is doing real work rather
   than being a modern flourish. The dialog is promoted to the browser's TOP
   LAYER, so it covers the navbar, the right drawer and the transaction hover
   card (z-index 1050, 1051 and 10600) without joining that ladder or having to
   outbid it — note there is no z-index anywhere below. It also brings the focus
   trap, the Escape key, aria-modal and inertness of the page behind it, four
   things a div-based overlay has to hand-roll and usually gets subtly wrong.
   The JS refuses to enable anything if showModal is missing, so there is never
   a live affordance without a working dialog behind it.

   THE COMPONENT IS DARK IN BOTH MODES, which is why its colours are local
   tokens instead of the global ramp. A scrim exists to remove the page so the
   image is the only lit thing; a light scrim over a light page removes nothing,
   and every image viewer worth copying is dark on both. That makes the mode
   tokens exactly wrong here, not merely unnecessary: on light --prism-text is
   #161b29 and --prism-border-strong is rgba(22,27,41,.24), so a caption and a
   hairline that "correctly" read the ramp would both disappear into the scrim.

   So the five --lightbox-* tokens below are declared once, on the component,
   with no light twin — the same arrangement --error-tone uses in ERROR PAGES,
   and the reason they are outside the --prism-* namespace: that namespace is
   the app's mode-flipping vocabulary and these deliberately do not flip. Only
   the focus ring reaches back out to --prism-accent, because a focus ring
   should follow the skin like every other focus ring in the app.
   ============================================================================ */

.prism-lightbox,
.prism-lightbox-trigger {
    /* Mode-invariant on purpose — see the section header above. Alpha-over-dark
       throughout, so every one of these composites the same way whether the
       page underneath is the near-black app ground or light mode's #dde1ea. */
    --lightbox-scrim:  rgba(6, 8, 16, 0.88);
    --lightbox-ink:    rgba(255, 255, 255, 0.94);
    --lightbox-ink-2:  rgba(255, 255, 255, 0.62);
    --lightbox-chrome: rgba(255, 255, 255, 0.12);
    --lightbox-edge:   rgba(255, 255, 255, 0.22);
}

/* ---- The dialog itself ---------------------------------------------------
   The scrim is painted on the dialog rather than on ::backdrop. Both would
   work, but ::backdrop only began inheriting custom properties from its
   originating element recently, and a scrim that silently resolves to nothing
   on an older engine is a full-screen transparent click-trap. The UA's own
   backdrop is zeroed out just below so the two never stack. */
.prism-lightbox {
    position: fixed;
    inset: 0;
    /* The UA stylesheet sizes a dialog to its content and caps it with
       calc(100% - 6px - 2em); all four are restated so it fills the viewport. */
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    margin: 0;
    padding: 0;
    border: 0;
    background: var(--lightbox-scrim);
    overflow: hidden;
    overscroll-behavior: contain;
}

.prism-lightbox::backdrop {
    background: transparent;
}

/* `dialog` is display:none until [open], so the centring is attached to [open]
   rather than to the element — otherwise this would fight the UA's own
   show/hide and the dialog would render while closed. */
.prism-lightbox[open] {
    display: flex;
    align-items: center;
    justify-content: center;
}

.prism-lightbox-figure {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    margin: 0;
    max-width: min(1600px, 92vw);
    max-height: 92vh;
    max-height: 92dvh;
}

.prism-lightbox-img {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    /* Short of the figure's 92% so the caption underneath always has room. */
    max-height: 78vh;
    max-height: 78dvh;
    object-fit: contain;
    border-radius: var(--prism-radius-card);
    border: 1px solid var(--lightbox-edge);
    /* The scrim doubles as the shadow colour: what pools under a lifted image
       here IS more of the scrim, and it saves a sixth token that would only
       ever hold another near-black. */
    box-shadow: 0 30px 90px var(--lightbox-scrim);
}

.prism-lightbox-caption {
    margin: 0;
    max-width: 60ch;
    text-align: center;
    font-size: 0.86rem;
    font-weight: 600;
    letter-spacing: 0.2px;
    color: var(--lightbox-ink-2);
}

/* Not every trigger has a caption. Without this the figure's gap still reserves
   14px under the image for an empty node. */
.prism-lightbox-caption:empty {
    display: none;
}

/* ---- Close button -------------------------------------------------------- */
.prism-lightbox-close {
    position: absolute;
    /* env() keeps it clear of a notch; max() keeps it honest on everything
       else, where the inset resolves to 0px. */
    top: max(14px, env(safe-area-inset-top, 0px));
    right: max(14px, env(safe-area-inset-right, 0px));
    display: flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border: 1px solid var(--lightbox-edge);
    border-radius: var(--prism-radius-pill);
    background: var(--lightbox-chrome);
    color: var(--lightbox-ink);
    cursor: pointer;
    transition: background-color .16s ease, transform .16s ease;
}

.prism-lightbox-close:hover {
    background: var(--lightbox-edge);
}

.prism-lightbox-close:active {
    transform: scale(0.94);
}

.prism-lightbox-close:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 3px;
}

/* The glyph is a MASK, not an <img> or a font icon: mask-mode reads only the
   alpha channel, so `stroke='%23000'` inside the data URI is a stencil and not
   a colour — what you see is background-color, i.e. the button's own currentColor.
   That is what keeps the icon on the token and off a literal, and it is also why
   the landing page can use this without FontAwesome having loaded. */
.prism-lightbox-close::before {
    --glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E");
    content: '';
    width: 16px;
    height: 16px;
    background-color: currentColor;
    -webkit-mask: var(--glyph) center / 16px 16px no-repeat;
            mask: var(--glyph) center / 16px 16px no-repeat;
}

/* ---- The trigger: anything the JS has made expandable --------------------
   Set by PrismLightbox.enable(), never written into markup by hand — see the
   module header for why a statically-marked trigger is a bug. */
.prism-lightbox-trigger {
    position: relative;
    cursor: zoom-in;
}

.prism-lightbox-trigger:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 4px;
}

/* The hint: a dark disc (::before) with a masked expand glyph over it
   (::after). Two pseudo-elements because one cannot carry both a fill and a
   mask — a mask clips the element it is on, disc included.

   The disc is --lightbox-scrim rather than the glassy --lightbox-chrome the
   close button uses, and the difference is where each one sits: the close
   button sits ON the scrim, while this sits on an arbitrary screenshot and has
   to stay legible over a white chart panel as readily as over a dark one. */
.prism-lightbox-trigger::before,
.prism-lightbox-trigger::after {
    content: '';
    position: absolute;
    right: 12px;
    bottom: 12px;
    box-sizing: border-box;
    width: 36px;
    height: 36px;
    pointer-events: none;
    opacity: 0;
    transition: opacity .18s ease;
}

.prism-lightbox-trigger::before {
    border: 1px solid var(--lightbox-edge);
    border-radius: var(--prism-radius-pill);
    background: var(--lightbox-scrim);
}

.prism-lightbox-trigger::after {
    --glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7'/%3E%3C/svg%3E");
    background-color: var(--lightbox-ink);
    -webkit-mask: var(--glyph) center / 15px 15px no-repeat;
            mask: var(--glyph) center / 15px 15px no-repeat;
}

.prism-lightbox-trigger:hover::before,
.prism-lightbox-trigger:hover::after,
.prism-lightbox-trigger:focus-visible::before,
.prism-lightbox-trigger:focus-visible::after {
    opacity: 1;
}

/* A touch device never hovers, so the hint would never appear and the frame
   would look inert. Show it permanently there instead. */
@media (hover: none) {
    .prism-lightbox-trigger::before,
    .prism-lightbox-trigger::after {
        opacity: 1;
    }
}

/* ---- Scroll lock ---------------------------------------------------------
   showModal() makes the page inert to clicks and tabbing, but it does not stop
   it scrolling under the dialog. --lightbox-gutter is set by the JS to the
   width the scrollbar was occupying, so removing the scrollbar does not shunt
   the whole page sideways by ~15px as the lightbox opens. */
html.prism-lightbox-open,
html.prism-lightbox-open body {
    overflow: hidden;
}

html.prism-lightbox-open body {
    padding-right: var(--lightbox-gutter, 0px);
}

/* ---- Motion --------------------------------------------------------------
   Two things here are load-bearing and both look like they could be tidied up.

   The closing pair needs [open] in the selector as well as .is-closing: the
   dialog is still open while it animates out, so `.prism-lightbox[open]`
   (0,2,1) would otherwise outrank a bare `.prism-lightbox.is-closing` (0,2,0)
   and the exit animation would never play.

   And the exit uses its OWN keyframes rather than the entrance ones run with
   `animation-direction: reverse`, which is the obvious way to write it and does
   not work. An animation is identified by its name: changing direction, timing
   or duration on an element whose animation has already FINISHED updates that
   same finished animation rather than starting a new one, so it never replays.
   The exit would snap to opacity 0 with no transition and fire no animationend,
   leaving the JS to close the dialog on its 400ms backstop instead — an instant
   disappearance followed by a quarter-second of nothing. A distinct name is
   what makes it a new animation. */
@keyframes prismLightboxIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes prismLightboxFigureIn {
    from { opacity: 0; transform: scale(0.96) translateY(10px); }
    to   { opacity: 1; transform: none; }
}

@keyframes prismLightboxOut {
    from { opacity: 1; }
    to   { opacity: 0; }
}

@keyframes prismLightboxFigureOut {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: scale(0.98) translateY(6px); }
}

.prism-lightbox[open] {
    animation: prismLightboxIn .18s ease both;
}

.prism-lightbox[open] .prism-lightbox-figure {
    animation: prismLightboxFigureIn .22s cubic-bezier(.22, .61, .36, 1) both;
}

.prism-lightbox[open].is-closing {
    animation: prismLightboxOut .16s ease both;
}

.prism-lightbox[open].is-closing .prism-lightbox-figure {
    animation: prismLightboxFigureOut .16s ease both;
}

@media (prefers-reduced-motion: reduce) {
    .prism-lightbox[open],
    .prism-lightbox[open] .prism-lightbox-figure,
    .prism-lightbox[open].is-closing,
    .prism-lightbox[open].is-closing .prism-lightbox-figure {
        animation: none;
    }

    .prism-lightbox-close,
    .prism-lightbox-trigger::before,
    .prism-lightbox-trigger::after {
        transition: none;
    }
}

@media (max-width: 576px) {
    .prism-lightbox-figure {
        gap: 10px;
        max-width: 96vw;
    }

    .prism-lightbox-img {
        max-height: 70vh;
        max-height: 70dvh;
    }

    .prism-lightbox-close {
        width: 38px;
        height: 38px;
    }

    .prism-lightbox-trigger::before,
    .prism-lightbox-trigger::after {
        right: 8px;
        bottom: 8px;
        width: 30px;
        height: 30px;
    }
}

/* =============================================================================
   ASSET TYPE CARDS
   Used in: home.html's #asset-types section — the three cards naming what an
   asset can be (residential property, commercial real estate, business).

   Here rather than in home.html's own <style> for the reason
   tests/test_page_local_css.py exists: the landing page already holds more CSS
   than this sheet does, and a rule in a template is outside every light-mode
   and contrast contract, all of which only read this file. So these are on
   --prism-* tokens, not the page's local --px-* set, and they follow the skin
   and flip for light mode like everything else. The card shell itself is still
   the landing page's .bento .cell; these are only the parts inside it.
   ========================================================================== */

/* The three cards carry different amounts of text, so their closing notes
   landed at three different heights and the row read as ragged. Absorbing the
   slack after the chips pins the notes to a common baseline while leaving
   .asset-type-note's own margin as the minimum gap — which `margin-top: auto`
   on the note itself would have collapsed to nothing on the tallest card. */
.asset-type-card {
    display: flex;
    flex-direction: column;
}

.asset-type-card .asset-type-chips {
    margin-bottom: auto;
}

/* Example assets. Display face because they are names, not prose. */
.asset-type-eg {
    margin-top: 14px;
    font-family: var(--prism-font-display);
    font-size: .82rem;
    letter-spacing: .01em;
    color: var(--prism-text-dim);
}

.asset-type-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 7px;
    margin-top: 16px;
}

/* Deliberately its own class rather than a resized .cat-chip: that one is the
   landing page's marquee chip, defined page-locally and sized for a 30px icon
   tile these do not have. Sharing it would have put a rule in this sheet whose
   base declaration lives in a template. */
.field-chip {
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--prism-border);
    background: var(--prism-surface-2);
    border-radius: 999px;
    padding: 5px 12px;
    font-size: .76rem;
    font-weight: 500;
    white-space: nowrap;
    color: var(--prism-text-muted);
}

.asset-type-note {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--prism-border);
    font-size: .84rem;
}

.asset-type-note i {
    flex: none;
    font-size: .78rem;
    color: var(--prism-accent);
}

.asset-type-footer {
    max-width: 74ch;
    margin: 26px auto 0;
    text-align: center;
    font-size: .97rem;
    line-height: 1.65;
    color: var(--prism-text-muted);
}

/* =============================================================================
   LANDING PAGE — COMPOSITION & MOTION
   Used in: home.html, whose <body> carries `px-landing`. Everything here is
   scoped to that class, so none of it can reach the app even though this sheet
   loads on every page. The scope also buys a class of specificity over
   home.html's own <style>, which loads after this file — that is what lets a
   rule here retune a page-local one without the page-local one being deleted.

   Here rather than in home.html's <style> for the reason given in the ASSET
   TYPE CARDS section above, and because tests/test_page_local_css.py's four
   ratchets were all at their ceiling: the landing page could not take one more
   rule. Colour comes from --prism-* tokens so this follows the skin and flips
   for light mode like everything else; geometry and timing are literals, which
   is what the rest of this sheet does (125 transitions, no timing tokens).

   The behavioural half is assets/js/prism-landing.js. The division is the
   usual one: JS writes a single custom property per scroll-linked value
   (--p, --hero-p, --page-p, --settle, --lit, --mx/--my) and every transform
   that consumes them is described here, once.

   Motion budget: only transform / opacity / filter animate, so nothing here
   triggers layout or paint on scroll. Every effect is off under
   prefers-reduced-motion (the block at the end of this section), and the
   scroll-linked ones are additionally off below 1100px where they cost
   battery and read as jank.
   ========================================================================== */

/* The pacing scale. The page used a flat 96px between every section with
   padding-top:0 on four of them, which ran Features, Asset types and Bento
   together as one undifferentiated slab. Three steps instead: -lg between
   chapters, -md within one, -sm from a heading to its content.

   In the --px-* namespace, not --prism-*, because these are the landing page's
   own vocabulary and nothing in the app has sections in this sense; adding
   them as --prism-* would put three tokens in the shared contract that exactly
   one page could ever reference. */
.px-landing {
    --px-beat-lg: clamp(88px, 11vw, 160px);
    --px-beat-md: clamp(64px, 7vw, 104px);
    --px-beat-sm: clamp(40px, 5vw, 64px);
    /* One entrance curve for everything that arrives, and the app's existing
       lift curve for everything that responds to a pointer. Mixing the two was
       what made the old reveals read as generic. */
    --px-ease-in: cubic-bezier(.16, 1, .3, 1);
    --px-ease-lift: cubic-bezier(.2, .7, .3, 1);
    /* The highlight that crosses the wordmark on open. An alpha ramp rather
       than a colour, so one band reads correctly over every stop of the
       gradient underneath it. */
    --px-sheen: linear-gradient(100deg,
                    rgba(255, 255, 255, 0)    34%,
                    rgba(255, 255, 255, 0.92) 47%,
                    rgba(255, 255, 255, 0.18) 55%,
                    rgba(255, 255, 255, 0)    66%);
}

/* --- Brand wordmark ------------------------------------------------------
   The landing page rendered the wordmark flat white while the in-app navbar
   and footer both filled it from --prism-wordmark, so the same word arrived
   in two different treatments depending on which door you came in.

   --prism-wordmark, NOT the page's own --px-brand. The two resolve
   identically here today, which is exactly why it is worth writing down:
   --px-brand reads --prism-brand, which is built from the accent triplet and
   follows all nine skins, and the brand is identity rather than a preference.
   The navbar's rule carries the same note, one section up.

   Gated to prism. The gradient is the Prism mark's own three hues, so under
   Doshi it would be a blue-violet wordmark beside a gold "D" — the same
   mismatch the mark's glow is already gated on. Doshi keeps the flat
   --px-text it renders today. */
body.px-landing[data-brand="prism"] .brand-text {
    background-image: var(--px-sheen), var(--prism-wordmark);
    background-size: 250% 100%, 100% 100%;
    background-repeat: no-repeat;
    background-position: -25% 0, 0 0;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
}

/* The sweep runs once, on open, and only in the top bar. The footer and
   mobile-menu lockups take the gradient without it: neither is on screen at
   open, so an animation there is one nobody is ever present to see fire. */
body.px-landing[data-brand="prism"] .nav .brand-text {
    animation: brandSweep 1450ms cubic-bezier(.33, .68, .36, 1) 1 both;
}

/* An oversized background layer INVERTS percentage positioning — the offset is
   pos x (box - layer), and (box - layer) is negative — so 110% -> -25% walks
   the sheen rightward and the highlight crosses the word left to right. Both
   endpoints put the band fully outside the box, so the resting state is the
   plain gradient with no bright patch parked in it. That is what the odd
   numbers buy, and why they are not round.

   The second pair is the gradient itself, held still. A layer sized 100% has a
   zero offset at every percentage, so it cannot drift while the first one
   moves — which is what lets one animation move the sheen alone without
   restating the brand fill in the keyframes.

   Reduced motion is handled by home.html's global `* { animation: none }`,
   and the resting position above is the finished state, so switching the
   animation off lands on the plain gradient rather than mid-sweep. */
@keyframes brandSweep {
    from { background-position: 110% 0, 0 0; }
    to   { background-position: -25% 0, 0 0; }
}

/* The page had no visible focus state at all — every control was reachable by
   keyboard and none of them said so. */
.px-landing a:focus-visible,
.px-landing button:focus-visible,
.px-landing summary:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 3px;
    border-radius: 8px;
}

/* --- Section rhythm ------------------------------------------------------ */
.px-landing .section-pad {
    padding: var(--px-beat-lg) 0;
}

/* A section that opens on a seam has already had its gap; giving it a full
   chapter's padding as well left ~260px of nothing above the heading. */
.px-landing .section-pad.has-seam {
    padding-top: var(--px-beat-md);
}

.px-landing .section-head {
    margin-bottom: var(--px-beat-sm);
}

/* The seam: the joint between two chapters. A hairline that fades out at both
   ends, carrying the chapter's eyebrow at its centre. This is the device that
   turns a list of sections into a sequence of them, and it is why the eyebrow
   moved out of .section-head on those sections rather than being duplicated.

   The label sits IN the seam, not on top of it. This was one element painting
   a full-width line with the eyebrow absolutely centred over the middle of it,
   and .eyebrow's background is `rgba(255,255,255,.03)` — so the hairline read
   straight through the pill and struck through the words. Both repairs that
   keep the overlay are worse: an opaque pill punches a hole in the ambient
   ground the whole page is composed over, and a transparent gap cut into the
   gradient has to be measured in percentages against a label whose width
   changes per chapter ("Pricing" is a third of "The whole workflow").

   Two flex siblings instead, so the line simply stops where the label starts
   and no width needs knowing. The box stays `height: 1px` with the items
   centred on it: the pill overflows it symmetrically, exactly as it did when
   it was absolutely positioned, so the section rhythm below is unchanged. */
.px-landing .px-seam {
    display: flex;
    align-items: center;
    gap: 20px;
    height: 1px;
    max-width: 1180px;
    margin: 0 auto var(--px-beat-sm);
    padding: 0 20px;
}

/* Each half fades out at the page edge and arrives at full strength beside the
   label, which is the same shape as the old 32%/68% gradient read from the
   middle outward. `scaleX(-1)` rather than a second gradient: one description
   of the fade, mirrored, so the two ends cannot drift apart. */
.px-landing .px-seam::before,
.px-landing .px-seam::after {
    content: '';
    flex: 1 1 0;
    min-width: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--prism-border-strong) 62%);
}

.px-landing .px-seam::after {
    transform: scaleX(-1);
}

/* Never shrink: the label is the content, the rules beside it are decoration.
   Left to flex defaults the pill is what gives way on a narrow viewport and
   its uppercase 2.4px-tracked text wraps to two lines inside a pill built for
   one. */
.px-landing .px-seam .eyebrow {
    flex: 0 0 auto;
}

/* Tiering the headings. Everything was clamp(2rem, 4vw, 2.9rem), so a
   sub-section read exactly as loudly as the chapter containing it. */
.px-landing .h-section {
    font-size: clamp(2.15rem, 3.6vw, 3rem);
    letter-spacing: -.028em;
}

.px-landing .h-sub {
    font-size: clamp(1.7rem, 2.6vw, 2.25rem);
    letter-spacing: -.022em;
}

.px-landing .lead {
    max-width: 62ch;
    text-wrap: pretty;
}

/* --- The reveal vocabulary ----------------------------------------------- */
/* Four members, where the page had one 28px fade applied identically to 44
   elements. Moved out of home.html so the system is covered by this sheet's
   contracts, and so a second page could use it.

   Still progressive enhancement: content is visible until home.html's inline
   <head> script confirms JS by putting `js` on <html>. */
.px-landing .reveal {
    transition: opacity .8s var(--px-ease-in),
                transform .8s var(--px-ease-in),
                filter .8s var(--px-ease-in);
    transition-delay: calc(var(--i, 0) * 60ms);
}

/* The blur is the whole difference between this and a jQuery fade: an element
   that only changes opacity reads as "a thing appeared", one that also comes
   into focus reads as "a thing arrived". */
.js .px-landing .reveal {
    opacity: 0;
    transform: translateY(18px);
    filter: blur(5px);
}

.js .px-landing .reveal.in {
    opacity: 1;
    transform: none;
    filter: none;
}

/* Framed screenshots travel further and slower. A large object animating at a
   small object's speed reads as weightless, which is the opposite of what a
   product shot should feel like. */
.px-landing .reveal-shot {
    transition: opacity 1s var(--px-ease-in), transform 1s var(--px-ease-in);
}

.js .px-landing .reveal-shot {
    opacity: 0;
    transform: translateY(24px) scale(.985);
}

.js .px-landing .reveal-shot.in {
    opacity: 1;
    transform: none;
}

/* Headline lines rise out of a mask rather than fading in. The padding/margin
   pair gives descenders room inside the clip without opening a gap between
   lines — without it the mask cuts the tails off every 'y' and 'p'. */
.px-landing .reveal-line {
    display: block;
    overflow: hidden;
    padding-bottom: .08em;
    margin-bottom: -.08em;
}

.px-landing .reveal-line > span {
    display: block;
    transition: transform .9s var(--px-ease-in), opacity .9s var(--px-ease-in);
    transition-delay: calc(var(--i, 0) * 90ms);
}

.js .px-landing .reveal-line > span {
    transform: translateY(110%);
    opacity: 0;
}

.js .px-landing .reveal-line.in > span {
    transform: none;
    opacity: 1;
}

/* --- Hero ---------------------------------------------------------------- */
.px-landing .hero {
    position: relative;
    /* Only has to clear the 78px fixed nav. It was 138px, and the 60px of air
       above the eyebrow was bought at the product shot's expense: the frame
       started at y=815, so a 1440x900 laptop saw 85px of it and a 13-inch
       1440x800 saw NONE. The single most persuasive thing on the page was
       below the fold on the machine most people read it on. */
    padding: 104px 0 0;
}

.px-landing .hero .wrap {
    position: relative;
    z-index: 1;
}

/* Ambient light that trails the pointer. This is the page's depth cue: without
   it the mesh behind the hero is indistinguishable from a flat image.

   --hero-p drifts it the other way from the copy (far layer). Opacity is the
   only thing that transitions — a transform transition here would lag the
   scroll-linked value and read as float. */
.px-landing .hero-bloom {
    position: absolute;
    inset: -10% 0 0;
    z-index: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 1.2s ease;
    background: radial-gradient(460px 460px at var(--bx, 50%) var(--by, 32%),
        rgba(var(--prism-accent3-rgb), .16), transparent 70%);
    transform: translate3d(0, calc(var(--hero-p, 0) * 24px), 0);
}

.px-landing .hero-bloom.on {
    opacity: 1;
}

/* The lockup. The mark used to sit at 76px on its own line above the eyebrow,
   ~90px below an identical mark in the fixed navbar, so the h1 was the fourth
   thing on the page and carried no more weight than its neighbours. */
.px-landing .hero-lockup {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 30px;
}

.px-landing .hero-logo {
    height: 44px;
    width: auto;
    margin: 0;
    filter: drop-shadow(0 8px 22px rgba(var(--prism-accent3-rgb), .42));
}

.px-landing .hero h1 {
    font-size: clamp(2.4rem, 5.4vw, 4.6rem);
    letter-spacing: -.032em;
    margin: 0 auto;
    max-width: none;
}

/* The headline is typeset, not flowed: each masked line has to be one line, or
   the mask reveals two and the effect reads as a bug. Below this width the
   lines wrap and the reveal degrades to a per-line fade, which is fine. */
@media (min-width: 760px) {
    .px-landing .hero h1 .reveal-line > span {
        white-space: nowrap;
    }
}

.px-landing .hero .lead {
    font-size: 1.14rem;
    margin: 20px auto 0;
    max-width: 58ch;
}

/* Dropped to a footnote. At 0.9rem in a 24px-gap row it read as a third tier
   of call to action competing with the two real ones above it. */
/* Scoped to the hero deliberately. `.hero-cta` is macros/cta.html's, and every
   marketing page's closing band renders it — so the 36px in prism-marketing.css
   is not ours to trim. This narrows the gap here and nowhere else. */
.px-landing .hero .hero-cta {
    margin-top: 28px;
}

.px-landing .hero-trust {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0;
    margin-top: 22px;
    font-size: .82rem;
    letter-spacing: .02em;
}

.px-landing .hero-trust span {
    padding: 0 20px;
    border-left: 1px solid var(--prism-border);
}

.px-landing .hero-trust span:first-child {
    border-left: 0;
}

.px-landing .hero-shot {
    /* A beat, not a chapter break: this is the same thought as the copy above
       it, and --px-beat-md (101px at 1440) pushed the frame past the fold. The
       section rhythm below still uses the full beats — those separate
       arguments, this separates a claim from its evidence. */
    margin: var(--px-beat-sm) auto 0;
    perspective: 1600px;
    perspective-origin: 50% 0%;
}

/* The whole hero pose, in one place. --settle runs 0 -> 1 once on load (the
   shot arriving); --hero-p runs 0 -> 1 across the first viewport (the shot
   standing up as you scroll into it). Both are written by prism-landing.js and
   neither is described anywhere else. 7deg and 26px of total travel: enough to
   register as depth, not enough to notice as an effect.

   --hero-p is set on `.hero`, so the copy / bloom / glow / beam inherit the
   same progress and the depth stack cannot desync. The shot is the near
   layer (largest travel, plus the 3D settle); the title is mid; atmosphere
   recedes. */
.px-landing .hero-stage {
    position: relative;
    z-index: 1;
    transform-style: preserve-3d;
    transform:
        rotateX(calc((1 - var(--settle, 0)) * 7deg + var(--hero-p, 0) * -3deg))
        translateY(calc((1 - var(--settle, 0)) * 48px + var(--hero-p, 0) * -26px))
        scale(calc(.965 + var(--settle, 0) * .035));
    transition: transform 1.1s var(--px-ease-in);
}

/* Midground of the hero's parallax stack. No transition: a lagged title
   against a settling shot is two easings fighting, and the copy has to
   track the scroll or the depth cue collapses into float. 14px against the
   shot's 26px is the whole point — same direction, less travel, so the
   product reads as nearer. */
.px-landing .hero-copy {
    position: relative;
    z-index: 1;
    transform: translate3d(0, calc(var(--hero-p, 0) * -14px), 0);
}

/* The glow and the spectrum beam sit just behind the frame, so they drift
   the far direction (positive Y) while the stage comes forward. The glow
   rule has to live here: home.html's page-local `::before` cannot take
   another property without growing a ratchet that is already at its ceiling. */
.px-landing .hero-shot::before {
    transform: translate3d(0, calc(var(--hero-p, 0) * 10px), 0);
}

.px-landing .hero-shot .beam {
    transform: translate3d(0, calc(var(--hero-p, 0) * 18px), 0);
}

/* The ambient mesh is already `position: fixed`, which is one kind of
   parallax. This is the other: it actually travels, slower than anything
   on the page, over the whole document. Landing-only — the in-app
   `body::before` must not move, because a working surface that drifts under
   a ledger is seasick, not cinematic.

   One number, because the oversize has to be *larger* than the travel and
   not merely equal to it. At -48px/48px the layer's top edge lands exactly
   on the viewport's at the bottom of the page: correct in arithmetic and one
   subpixel from a hairline of untinted --px-bg across the top of the screen,
   at the one scroll position nobody re-checks. 1.5x leaves real slack at both
   ends and cannot be got wrong by editing one of two numbers. */
body.px-landing::before {
    --mesh-travel: 48px;
    inset: calc(var(--mesh-travel) * -1.5) 0;
    transform: translate3d(0, calc(var(--page-p, 0) * var(--mesh-travel)), 0);
}

/* --- Framed screenshots -------------------------------------------------- */
/* A 1px border and one big shadow made these read as images on a page. A top
   highlight, a contact shadow under a cast one, and a halo that comes up as
   the frame enters view make them read as objects under a light. */
/* position: relative is load-bearing, not tidiness — only the hero's frame
   carried it page-locally, so without it the highlight below would position
   itself against whatever ancestor happened to be positioned. */
.px-landing .browser {
    position: relative;
    transition: transform .45s var(--px-ease-lift),
                box-shadow .45s ease,
                border-color .45s ease;
}

.px-landing .browser::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    border-radius: 16px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10),
                inset 0 0 0 1px rgba(255, 255, 255, .02);
}

.px-landing .shot-lit .browser {
    box-shadow:
        0 2px 8px rgba(0, 0, 0, .5),
        0 44px 90px -40px rgba(0, 0, 0, .85),
        0 24px 90px -50px rgba(var(--prism-accent3-rgb), calc(var(--lit, 0) * .55));
}

/* Deliberately a lift and a warmer edge, not a tilt. Rotating a screenshot of
   a ledger under the cursor is where cinematic turns into a toy. */
.px-landing .shot-lit .browser:hover {
    transform: translateY(-6px);
    border-color: rgba(var(--prism-accent3-rgb), .42);
}

/* Scroll parallax. The copy column stays still and the shot drifts against it,
   which is what stops the pair reading as two things stacked side by side. */
.px-landing .par {
    transform: translate3d(0, calc(var(--p, 0) * -22px), 0);
}

/* --- Trust band: the seam between the hero and the story ----------------- */
.px-landing .trust-band {
    position: relative;
    margin-top: var(--px-beat-md);
}

/* A single spectrum stripe crossing the top hairline every 14 seconds. The
   quietest available way to mark the joint — most of the time there is nothing
   there at all. */
.px-landing .trust-band::before {
    content: '';
    position: absolute;
    top: -1px;
    left: 0;
    height: 1px;
    width: 34%;
    opacity: .32;
    background: var(--prism-spectrum);
    animation: prism-landing-sheen 14s linear infinite;
}

@keyframes prism-landing-sheen {
    0%   { transform: translateX(-110%); }
    100% { transform: translateX(400%); }
}

/* Auto columns rather than equal ones: the four claims are different lengths,
   and equal columns wrapped the first and last onto two lines each. */
.px-landing .trust-band .wrap {
    display: grid;
    grid-template-columns: repeat(4, auto);
    justify-content: space-between;
    align-items: center;
    gap: 26px;
    flex-wrap: nowrap;
}

/* --- Feature rows -------------------------------------------------------- */
/* The shot takes the wider column on both sides, and bleeds past the container
   on its outer edge so the product runs off the page instead of sitting inside
   a box on it. */
.px-landing .feature-row {
    grid-template-columns: 1fr 1.12fr;
    gap: 64px;
}

.px-landing .feature-row.flip {
    grid-template-columns: 1.12fr 1fr;
}

/* Grid items default to min-content, and the fake browser bar's URL is
   white-space: nowrap — without this the track cannot shrink and the row
   overflows the page on a phone. */
.px-landing .feature-row > * {
    min-width: 0;
}

.px-landing .feature-row + .feature-row {
    margin-top: var(--px-beat-lg);
}

@media (min-width: 1100px) {
    .px-landing .feature-row:not(.flip) .feat-shot { margin-right: -7%; }
    .px-landing .feature-row.flip .feat-shot { margin-left: -7%; }
}

/* The bleed above has to have somewhere to bleed INTO, and between 1100px and
   roughly 1260px it does not: 7% of the shot column is wider than the margin
   .wrap leaves beside it, so the frame ran off the VIEWPORT rather than off the
   page and gave the whole document a horizontal scrollbar. Clipping at the
   section is what "runs off the page" actually means.

   `clip`, never `hidden`: hidden makes this a scroll container, which changes
   what `position: sticky` and programmatic scrolling do inside it. `clip`
   paints nothing outside the box and creates no such container. X only — the
   reveal transforms move on Y and must not be cut. */
.px-landing #features {
    overflow-x: clip;
}

/* --- Bento --------------------------------------------------------------- */
/* Moved here from home.html rather than merely extended. The ASSET TYPE CARDS
   section above already depended on .bento .cell for its card shell while
   living in this sheet, so the component was split across two files with the
   base half outside every contract. */
.px-landing .bento {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 18px;
}

.px-landing .bento .cell {
    position: relative;
    overflow: hidden;
    padding: 26px;
    border-radius: var(--prism-radius-card);
    border: 1px solid var(--prism-border);
    background: linear-gradient(160deg, var(--prism-veil-2), var(--prism-veil-1));
    transition: transform .3s var(--px-ease-lift), border-color .3s ease, box-shadow .3s ease;
}

.px-landing .bento .cell > * {
    position: relative;
    z-index: 1;
}

/* The spotlight follows the pointer across the whole grid rather than sitting
   at the top of whichever cell is hovered, so the grid reads as one lit
   surface instead of ten independent tiles. --mx/--my come from one delegated
   listener in prism-landing.js. */
.px-landing .bento .cell::after {
    content: '';
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity .35s ease;
    background: radial-gradient(280px 280px at var(--mx, 50%) var(--my, 0%),
        rgba(var(--prism-accent3-rgb), .20), transparent 70%);
}

.px-landing .bento:hover .cell::after {
    opacity: 1;
}

.px-landing .bento .cell:hover {
    transform: translateY(-4px);
    border-color: var(--prism-border-strong);
    box-shadow: 0 20px 50px -24px rgba(0, 0, 0, .8);
}

.px-landing .bento .c-3 { grid-column: span 3; }
.px-landing .bento .c-2 { grid-column: span 2; }
.px-landing .bento .c-6 { grid-column: span 6; }

.px-landing .cell .b-ic {
    width: 42px;
    height: 42px;
    border-radius: 11px;
    display: grid;
    place-items: center;
    margin-bottom: 16px;
    font-size: 1.05rem;
    color: var(--prism-text);
    border: 1px solid var(--prism-border-strong);
    background: linear-gradient(135deg,
        rgba(var(--prism-accent-rgb), .18) 0%,
        rgba(var(--prism-accent3-rgb), .16) 52%,
        rgba(var(--prism-accent2-rgb), .18) 100%);
}

.px-landing .cell h4 {
    font-size: 1.18rem;
    margin-bottom: 8px;
}

.px-landing .cell p {
    margin: 0;
    font-size: .94rem;
    color: var(--prism-text-muted);
}

.px-landing .cell .b-badge {
    position: absolute;
    top: 18px;
    right: 18px;
    z-index: 2;
    padding: 3px 9px;
    border-radius: var(--prism-radius-pill);
    font-size: .6rem;
    font-weight: 800;
    letter-spacing: .6px;
    text-transform: uppercase;
}

/* --- The pinned product scene -------------------------------------------- */
/* Three frames at a third of the container each gave every screenshot about
   360px, at which point none of them proves anything. One frame at full width,
   held while the page scrolls past it, cross-fading through the three views.

   The one place on this page where pinning earns itself: the content is three
   states of a single object, so holding the object still and changing its
   state is what the section actually means.

   Everything that hides is gated on `px-motion`, which prism-landing.js sets
   when it actually starts. The BASE state — what you get with no JS, with a
   dead script, or under reduced motion — is the four shots stacked with their
   captions, all visible. Ungated, the `data-on="0"` rule below hid three of
   the four screenshots for anyone without JS, leaving an inert scene showing
   one image and no working control. Gate the enhancement, never the content. */
.px-landing .scene-track {
    position: relative;
}

/* There is no 400vh track here any more, and its absence is the point. The
   scene used to pin: four viewport-heights of scrolling during which the page
   did not advance, it swapped in place — 4,167px, 4.6 screens, to make one
   argument in 200 words. It was the single biggest reason this page felt like
   it never stopped.

   The four views are TABS now. Same four shots, same cross-fade, same overlay
   at `inset: 0` (which is still why they must share one aspect ratio — and why
   the drawer shots are captured 16:10 like everything else even though they
   need a much wider viewport to render the drawer unclamped; width is what the
   clamp reads, not shape — see DRAWER_GEOMETRY in tests/capture_screenshots.py).
   What changed is who moves them: prism-landing.js §10 on a click, rather than
   scroll position on the way past.

   These were briefly four short WebM clips of the drawer arriving. Removed: the
   clips only played once the scene was pinned AND above 1100px, so most
   visitors never saw the motion they were paying ~320 KB each to download. */

.px-landing .scene-grid {
    display: block;
}

.px-motion .px-landing .scene-grid {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 48px;
    align-items: center;
}

/* The stepper IS the scene now, so it shows at every width — it used to be
   hidden below 1100px, where the four views simply stacked. Still gated on
   `px-motion` and not on nothing: without the script the buttons move nothing,
   and the base state below is all four views visible with their captions. */
.px-landing .scene-steps {
    display: none;
    flex-direction: column;
    gap: 2px;
}

.px-motion .px-landing .scene-steps {
    display: flex;
}

/* A <button>, so it is focusable and operable from a keyboard. The reset is
   here rather than inline: border is zeroed and then the left rule put back,
   because that rule is the stepper's spine and the fill below rides on it. */
.px-landing .scene-step {
    position: relative;
    display: block;
    width: 100%;
    padding: 16px 0 16px 22px;
    background: none;
    border: 0;
    border-left: 1px solid var(--prism-border);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.px-landing .scene-step::before {
    content: '';
    position: absolute;
    left: -1px;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--prism-brand);
    transform: scaleY(0);
    transform-origin: top;
    transition: transform .45s var(--px-ease-in);
}

.px-landing .scene-step.on::before {
    transform: scaleY(1);
}

.px-landing .scene-step b {
    display: block;
    font-family: var(--prism-font-display);
    font-size: 1.02rem;
    color: var(--prism-text-dim);
    transition: color .35s ease;
}

/* The inactive steps carry their description collapsed rather than hidden, so
   the column's height does not change as the scene advances. */
.px-landing .scene-step span {
    display: block;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    font-size: .86rem;
    color: var(--prism-text-muted);
    transition: opacity .35s ease, max-height .45s var(--px-ease-in), margin-top .35s ease;
}

.px-landing .scene-step.on b {
    color: var(--prism-text);
}

.px-landing .scene-step.on span {
    max-height: 60px;
    opacity: 1;
    margin-top: 5px;
}

.px-landing .scene-stage {
    position: relative;
}

.px-landing .scene-stage > .shot {
    transition: opacity .55s var(--px-ease-in), transform .55s var(--px-ease-in);
}

.px-landing .scene-stage > .shot + .shot {
    margin-top: 40px;
}

.px-motion .px-landing .scene-stage > .shot + .shot {
    position: absolute;
    inset: 0;
    /* Undo the stacked fallback's spacing. `inset: 0` sets top: 0, and a
       margin-top on an absolutely positioned box is added to it — so the
       second and third views rendered 40px below the first and the cross-fade
       dropped instead of happening in place. */
    margin-top: 0;
}

.px-motion .px-landing .scene-stage > .shot[data-on="0"] {
    opacity: 0;
    transform: scale(.985);
    pointer-events: none;
}

/* The drawer view's mock: the only frame on this page that has to show two
   surfaces at once, because the drawer is not a page — it is what opens over
   the one you are already reading, and a picture of it alone would just look
   like a narrow page. Grid rather than the flex .mk-row next door: the drawer
   needs a fixed share of the frame, and `minmax(0, 1fr)` is what stops the
   transaction names' `text-overflow: ellipsis` from being defeated by the
   list's own min-content width.

   Both rules live here rather than in home.html's <style>: that block's rule
   count is ratcheted against this sheet (tests/test_page_local_css.py) and is
   at its ceiling. Everything below is a fallback anyway — the frame carries
   data-shot="details-drawer" and swaps itself for the real screenshot. */
.px-landing .mock-with-drawer {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 42%;
    gap: 14px;
    align-items: start;
}

/* The row the drawer is about. Without it the two panels read as two unrelated
   lists side by side, which is the one thing this frame exists not to say. */
.px-landing .mock-with-drawer .txn.on {
    box-shadow: inset 2px 0 0 var(--prism-accent);
    padding-left: 10px;
}

/* The captions are the stepper while the scene is pinned, and captions again
   in every fallback — so they are hidden only when the scene is actually
   running, not by default. */
.px-motion .px-landing .scene-cap {
    display: none;
}

.px-landing .scene-cap {
    margin-top: 16px;
    text-align: center;
    font-size: .92rem;
    font-weight: 600;
    color: var(--prism-text-muted);
}

.px-landing .scene-cap b {
    color: var(--prism-text);
    font-family: var(--prism-font-display);
}

/* --- Navbar -------------------------------------------------------------- */
.px-landing .nav.scrolled {
    padding: 10px 0;
}

.px-landing .nav-links .lnk {
    position: relative;
    padding: 4px 0;
}

.px-landing .nav-links .lnk::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transition: transform .14s var(--px-ease-lift);
}

.px-landing .nav-links .lnk:hover::after,
.px-landing .nav-links .lnk.active::after {
    transform: scaleX(1);
}

.px-landing .nav-links .lnk.active {
    color: var(--prism-text);
}

.px-landing .nav-menu-btn {
    display: none;
    width: 42px;
    height: 42px;
    align-items: center;
    justify-content: center;
    border-radius: 12px;
    border: 1px solid var(--prism-border-strong);
    background: var(--prism-surface-2);
    color: var(--prism-text);
    font-size: 1.05rem;
    cursor: pointer;
}

/* The mobile sheet. Opaque on purpose and allowed to be: it floats over
   arbitrary content, which is the exemption SURFACE_EXCLUDE describes, and a
   translucent navigation sheet over a moving page is unreadable. */
.px-landing .px-nav-menu {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    flex-direction: column;
    padding: 20px 24px 40px;
    background: var(--prism-drawer-ground);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity .28s var(--px-ease-in),
                transform .28s var(--px-ease-in),
                visibility .28s;
}

.px-landing .px-nav-menu.open {
    opacity: 1;
    visibility: visible;
    transform: none;
}

.px-landing .px-nav-menu .menu-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 34px;
}

.px-landing .px-nav-menu .menu-links {
    display: flex;
    flex-direction: column;
}

.px-landing .px-nav-menu .menu-links a {
    padding: 14px 0;
    border-bottom: 1px solid var(--prism-border);
    font-family: var(--prism-font-display);
    font-size: 1.6rem;
    font-weight: 600;
}

.px-landing .px-nav-menu .menu-actions {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-top: auto;
    padding-top: 30px;
}

/* --- Buttons ------------------------------------------------------------- */
.px-landing .btn {
    position: relative;
    overflow: hidden;
}

/* A single sheen crossing on hover. Enough to acknowledge the pointer without
   the button becoming an animation. */
.px-landing .btn::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: -120%;
    width: 44%;
    pointer-events: none;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .30), transparent);
    transition: left .7s var(--px-ease-in);
}

.px-landing .btn:hover::after {
    left: 130%;
}

.px-landing .btn .fa-arrow-right {
    transition: transform .3s var(--px-ease-lift);
}

.px-landing .btn:hover .fa-arrow-right {
    transform: translateX(3px);
}

/* --- Pricing ------------------------------------------------------------- */
/* A pill that slides between the two intervals, rather than a background
   jumping from one button to the other. --pill-w/--pill-x are measured in
   prism-landing.js because the two labels are different widths. */
.px-landing .price-toggle {
    position: relative;
}

.px-landing .price-toggle::before {
    content: '';
    position: absolute;
    top: 5px;
    bottom: 5px;
    left: 5px;
    width: var(--pill-w, 108px);
    border-radius: var(--prism-radius-pill);
    background: var(--prism-brand);
    box-shadow: 0 6px 18px -8px rgba(var(--prism-accent3-rgb), .7);
    transform: translateX(var(--pill-x, 0));
    transition: transform .38s var(--px-ease-in), width .38s var(--px-ease-in);
}

.px-landing .price-toggle button {
    position: relative;
    z-index: 1;
}

/* The pill is the fill now, so the active button only changes colour. */
.px-landing .price-toggle button.active {
    background: transparent;
    box-shadow: none;
}

@keyframes prism-landing-arrive {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

.px-landing .tier-price .amt.arrived {
    animation: prism-landing-arrive .26s var(--px-ease-in);
}

/* Digits that shift between rows is the detail that says nobody looked. */
.px-landing .kpi .val,
.px-landing .txn .amt,
.px-landing .ptable .money,
.px-landing .tier-price .amt,
.px-landing .compare .val {
    font-variant-numeric: tabular-nums;
}

/* --- FAQ ----------------------------------------------------------------- */
/* <details> snaps open. The 0fr -> 1fr grid row is the only way to transition
   to an intrinsic height without measuring it in JS. */
.px-landing .faq-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .38s var(--px-ease-in);
}

.px-landing .faq-item[open] .faq-body {
    grid-template-rows: 1fr;
}

.px-landing .faq-body > div {
    overflow: hidden;
}

/* --- Final call to action ------------------------------------------------ */
.px-landing .final-card {
    padding: 76px 32px;
}

.px-landing .final-card::before {
    transform: translate(-50%, calc(var(--p, 0) * 26px));
}

/* Draws itself as the card arrives instead of being present from the start. */
.px-landing .spectrum-line {
    transform: scaleX(0);
    transform-origin: center;
    transition: transform 1s var(--px-ease-in) .25s;
}

.px-landing .spectrum-line.in {
    transform: scaleX(1);
}

.px-landing footer {
    margin-top: var(--px-beat-lg);
}

/* The trust line in the footer's bottom row is a link to /privacy-and-security.
   `inherit` rather than a colour of its own: it should read as the same quiet
   sentence it was before it became clickable, and only announce itself on
   hover. Without this it takes the default link colour and shouts from the one
   row of the page that is meant to be almost invisible. */
.px-landing .foot-bottom a {
    color: inherit;
    transition: color 0.2s ease;
}

.px-landing .foot-bottom a:hover,
.px-landing .foot-bottom a:focus-visible {
    color: var(--px-text);
    text-decoration: none;
}

/* NO content-visibility here, deliberately, and this is the second attempt.
   `content-visibility: auto` on the below-the-fold sections is a real
   first-paint saving on a page this long, and it is also incompatible with
   everything else in this section: it makes the layout of unrendered content
   lazy, so an element's absolute offset changes as you scroll past it. When the
   showcase was scroll-driven, its track measured 8396px from the top of the
   document at load and 6874px once the sections above it had rendered — a
   1522px drift, most of a viewport, which changed which view the scene showed
   at a given scroll position on a first visit.

   The showcase is tabs now and no longer cares, but the hero parallax and the
   --page-p mesh still read absolute offsets from the same measure(), so the
   incompatibility stands. Either the page skips layout it has not painted, or
   scroll position means something. It cannot be both. */

/* --- Responsive ---------------------------------------------------------- */
/* Below 1100px the two-column scene stacks — tabs above, stage below — and the
   parallax stops: on a narrow screen the travel is a large fraction of the
   viewport and reads as drift rather than depth.

   The tabs STAY. They used to be hidden here and the four views stacked out
   full-length instead, which cost a phone 2,682px to show what one tapped view
   shows in ~250px — and made the section's whole "click anything" argument
   unavailable on the device most likely to meet it first. Nothing about the
   control needs a wide screen; only the 220px side rail did. */
@media (max-width: 1099px) {
    .px-motion .px-landing .scene-grid { display: block; }
    .px-motion .px-landing .scene-steps { margin-bottom: 26px; }
    .px-landing .par,
    body.px-landing::before { transform: none; }
}

@media (max-width: 900px) {
    /* Gated on the engine, like everything else it depends on. The button does
       nothing without prism-landing.js — setMenu is what opens the sheet — so
       showing it when the script is absent or dead offers a control that
       cannot work. The links beside it are hidden at this width regardless, so
       what a no-JS phone gets is no navigation chrome and a page it scrolls,
       rather than a button that ignores it. */
    .px-motion .px-landing .nav-menu-btn { display: inline-flex; }
    /* The header's call to action goes with the links it sits beside. It is a
       duplicate — the sheet carries the same two buttons, and the hero's own
       CTA is a screen away — and at this width it is what pushes the menu
       button off the edge. Worst with the signup gate closed, where the label
       is "Request an invite" rather than "Start free", and worse again on the
       brand whose name is "Doshi Investments LLC": measured with both, the
       button's right edge landed at 405px in a 390px viewport. `overflow-x:
       clip` on <html> contains that rather than letting it scroll, so the
       control was not merely awkward to reach, it was unreachable — and it is
       the only way to the navigation on a phone. */
    .px-landing .nav-cta .btn-primary { display: none; }
    /* .feature-row.flip is 0-2-0, so the single-column collapse has to name it
       too — specificity beats source order and the flipped rows stayed in two
       columns, overflowing the page. */
    .px-landing .feature-row,
    .px-landing .feature-row.flip { grid-template-columns: 1fr; gap: 34px; }
    .px-landing .bento { grid-template-columns: repeat(2, 1fr); }
    .px-landing .bento .c-3,
    .px-landing .bento .c-2,
    .px-landing .bento .c-6 { grid-column: span 2; }
    .px-landing .trust-band .wrap { grid-template-columns: repeat(2, auto); justify-content: start; }
    /* No 3D and no pointer light on a touch device: neither can be shown and
       both cost frames. */
    .px-landing .hero-shot { perspective: none; }
    .px-landing .hero-stage,
    .px-landing .hero-copy,
    .px-landing .hero-shot::before,
    .px-landing .hero-shot .beam { transform: none; }
    .px-landing .hero-bloom { display: none; }
}

@media (max-width: 560px) {
    /* Same reasoning as the desktop rule, and it matters more here: the nav is
       75px and the shot started at y=930 in an 844px viewport, so a phone saw
       none of it at all. */
    .px-landing .hero { padding: 92px 0 0; }
    .px-landing .hero-trust span { padding: 4px 12px; border-left: 0; }
    .px-landing .trust-band .wrap { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
    .px-landing .reveal,
    .px-landing .reveal-shot,
    .px-landing .reveal-line > span,
    .js .px-landing .reveal,
    .js .px-landing .reveal-shot,
    .js .px-landing .reveal-line > span {
        opacity: 1;
        transform: none;
        filter: none;
        transition: none;
    }
    .px-landing .hero-stage,
    .px-landing .hero-copy,
    .px-landing .hero-shot::before,
    .px-landing .hero-shot .beam,
    .px-landing .par,
    .px-landing .final-card::before,
    body.px-landing::before { transform: none; }
    .px-landing .spectrum-line { transform: scaleX(1); }
    .px-landing .hero-bloom { display: none; }
    .px-landing .trust-band::before { animation: none; }
    .px-landing .btn::after { display: none; }
    /* The scene shows all four views at once and drops the tabs. A cross-fade
       IS motion, and swapping the picture under someone who asked for less of
       it is exactly the surprise the preference exists to prevent — so this is
       the one place the four still stack. prism-landing.js agrees from the
       other side: onMotionPreferenceChange calls setSceneIndex(-1), which marks
       every view live rather than leaving three inert but visible. (There is no
       pin left to switch off here — see the scene block above.) */
    .px-motion .px-landing .scene-grid { display: block; }
    .px-motion .px-landing .scene-steps { display: none; }
    .px-motion .px-landing .scene-stage > .shot + .shot { position: static; margin-top: 40px; }
    .px-motion .px-landing .scene-stage > .shot,
    .px-motion .px-landing .scene-stage > .shot[data-on="0"] { opacity: 1; transform: none; pointer-events: auto; }
    .px-motion .px-landing .scene-cap { display: block; }
    .px-landing .faq-body { transition: none; }
}

/* ============================================================================
   LEGAL PAGES — PRIVACY & SECURITY, TERMS
   Used in: layouts/legal-base.html — legal/privacy-and-security.html,
   legal/terms.html.

   Everything here is scoped to `.px-legal`, the body class the layout sets.
   prism-common.css loads on every page, so an unscoped `.legal-doc` would be
   live in the product too; the scope is also the extra specificity that lets a
   page retune one of these rules without deleting it.

   The device is a MARGIN — a numbered rail down the left of the document, the
   way a legal instrument or a ledger is ruled. It is what makes a policy page
   feel like a document you can cite a line of rather than a wall of marketing
   copy, and it costs one pseudo-element. The sticky contents list opposite it
   is the other half: these pages are long by nature, and a reader who arrives
   from a footer link almost always wants one specific section.

   The rail and the contents list are progressive. The rail is decoration. The
   list is plain anchor links that work with no JS at all; only the active
   highlight is scripted, and it is added, never used to hide anything.

   Mode-agnostic throughout — every colour is a token, so light mode is the
   usual token flip with no `.white-content` override. Nothing here paints an
   opaque background: these are content surfaces and the ambient mesh has to
   show through them like everywhere else in the app.
   ============================================================================ */

.px-legal .legal-page {
    max-width: 1180px;
    margin: 0 auto;
    padding: 0 15px 80px;
}

/* ===== MASTHEAD ===== */

.px-legal .legal-masthead {
    position: relative;
    padding: 26px 0 46px;
    text-align: center;
}

/* Ambient bloom behind the title, the same device the error stage uses, so a
   page that opens on prose still reads as lit rather than as text stranded at
   the top of an empty viewport. */
.px-legal .legal-masthead::before {
    content: '';
    position: absolute;
    top: -10%;
    left: 50%;
    width: min(680px, 100%);
    height: 300px;
    transform: translateX(-50%);
    background: radial-gradient(ellipse at center,
                rgba(var(--prism-accent-rgb), 0.13) 0%,
                rgba(var(--prism-accent2-rgb), 0.06) 46%,
                rgba(var(--prism-accent-rgb), 0) 72%);
    pointer-events: none;
    z-index: 0;
}

.px-legal .legal-eyebrow {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 18px;
    padding: 7px 18px;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--prism-text-2);
    background: var(--prism-tint-2);
    border: 1px solid var(--prism-border);
}

.px-legal .legal-eyebrow i {
    font-size: 0.8rem;
    color: var(--prism-accent);
}

.px-legal .legal-title {
    position: relative;
    z-index: 1;
    margin: 0 0 18px;
    font-size: clamp(2.1rem, 5.4vw, 3.2rem);
    font-weight: 800;
    line-height: 1.08;
    letter-spacing: -0.02em;
}

.px-legal .legal-lede {
    position: relative;
    z-index: 1;
    max-width: 700px;
    margin: 0 auto;
    font-size: clamp(1rem, 1.7vw, 1.12rem);
    line-height: 1.7;
    color: var(--prism-text-2);
}

.px-legal .legal-stamp {
    position: relative;
    z-index: 1;
    margin: 26px 0 0;
    font-size: 0.78rem;
    letter-spacing: 0.06em;
    color: var(--prism-text-dim);
}

/* ===== PLEDGE STRIP =====
   Four one-line promises directly under the title. This is the part a reader
   who will not read the document still leaves with, so each one states a fact
   the sections below then substantiate — never a claim with nothing under it. */

.px-legal .legal-pledges {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 14px;
    margin: 0 0 58px;
}

.px-legal .legal-pledge {
    position: relative;
    padding: 20px 20px 20px 22px;
    border-radius: 14px;
    background: var(--prism-tint-1);
    border: 1px solid var(--prism-border);
    overflow: hidden;
}

/* The accent edge, not an accent fill: four saturated cards in a row would
   read as an alert strip. */
.px-legal .legal-pledge::before {
    content: '';
    position: absolute;
    top: 14px;
    bottom: 14px;
    left: 0;
    width: 2px;
    border-radius: 0 2px 2px 0;
    background: linear-gradient(180deg, var(--prism-accent), var(--prism-accent2));
}

.px-legal .legal-pledge i {
    display: block;
    margin-bottom: 12px;
    font-size: 1.05rem;
    color: var(--prism-accent);
}

.px-legal .legal-pledge-term {
    display: block;
    margin-bottom: 6px;
    font-size: 0.94rem;
    font-weight: 700;
    color: var(--prism-text);
}

.px-legal .legal-pledge-detail {
    display: block;
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--prism-text-muted);
}

/* ===== LAYOUT: CONTENTS RAIL + DOCUMENT ===== */

.px-legal .legal-layout {
    display: grid;
    grid-template-columns: 232px minmax(0, 1fr);
    gap: 54px;
    align-items: start;
}

.px-legal .legal-contents {
    position: sticky;
    top: 96px;
    /* Bounded, or a long document's rail outruns the viewport it is pinned in.
       The terms page has 17 entries and several wrap to two lines, so the list
       runs ~830px — taller than the space under a 96px offset on any laptop
       shorter than about 950px. Without this the last few sections are simply
       unreachable from the rail until you have already scrolled to them, which
       is the one moment you no longer need it. `100dvh` (with a vh fallback for
       browsers that lack it) rather than `100vh` because mobile browsers report
       vh as the viewport with the URL bar RETRACTED, so a vh-bounded rail is
       taller than the screen for as long as the bar is showing — which is
       exactly when the page has just loaded. */
    max-height: calc(100vh - 120px);
    max-height: calc(100dvh - 120px);
    overflow-y: auto;
    /* Reaching the end of the rail should not start scrolling the document
       underneath it — the rail and the page scroll different things. */
    overscroll-behavior: contain;
    /* Room for the scrollbar so it never sits on top of the link text. */
    padding-right: 4px;
}

/* Thin, and only present when it is doing something. The rail is chrome beside
   the document, so a full-width track next to it would read as a second page. */
.px-legal .legal-contents::-webkit-scrollbar { width: 6px; }

.px-legal .legal-contents::-webkit-scrollbar-track { background: transparent; }

.px-legal .legal-contents::-webkit-scrollbar-thumb {
    background: var(--prism-fill-1);
    border-radius: 10px;
}

.px-legal .legal-contents::-webkit-scrollbar-thumb:hover { background: var(--prism-fill-2); }

.px-legal .legal-contents-heading {
    margin: 0 0 14px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--prism-text-dim);
}

.px-legal .legal-contents ol {
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: legal-toc;
}

.px-legal .legal-contents li { counter-increment: legal-toc; }

.px-legal .legal-contents a {
    display: flex;
    gap: 10px;
    padding: 8px 12px 8px 10px;
    border-left: 2px solid var(--prism-border-faint);
    font-size: 0.87rem;
    line-height: 1.45;
    color: var(--prism-text-muted);
    transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}

.px-legal .legal-contents a::before {
    content: counter(legal-toc, decimal-leading-zero);
    flex-shrink: 0;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--prism-text-faint);
    font-variant-numeric: tabular-nums;
}

.px-legal .legal-contents a:hover,
.px-legal .legal-contents a:focus-visible {
    color: var(--prism-text);
    background: var(--prism-tint-1);
    border-left-color: var(--prism-accent-border);
    text-decoration: none;
}

.px-legal .legal-contents a[aria-current="true"] {
    color: var(--prism-text);
    border-left-color: var(--prism-accent);
    background: var(--prism-tint-1);
}

.px-legal .legal-contents a[aria-current="true"]::before { color: var(--prism-accent); }

/* ===== THE DOCUMENT ===== */

.px-legal .legal-doc {
    counter-reset: legal-section;
    max-width: 74ch;
}

.px-legal .legal-section {
    position: relative;
    counter-increment: legal-section;
    padding: 0 0 46px 0;
    /* Anchored sections land under the fixed navbar without this. */
    scroll-margin-top: 96px;
}

.px-legal .legal-section + .legal-section { padding-top: 42px; }

/* The rule between sections is the margin's own line, drawn short so the
   numeral below sits ON it rather than under a full-width divider. */
.px-legal .legal-section + .legal-section::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 64px;
    height: 1px;
    background: linear-gradient(90deg, var(--prism-accent-border), transparent);
}

.px-legal .legal-section-head {
    display: flex;
    align-items: baseline;
    gap: 14px;
    margin: 0 0 18px;
}

.px-legal .legal-section-num {
    flex-shrink: 0;
    width: 34px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--prism-accent);
    font-variant-numeric: tabular-nums;
}

.px-legal .legal-section-num::before {
    content: counter(legal-section, decimal-leading-zero);
}

.px-legal .legal-section h2 {
    margin: 0;
    font-size: clamp(1.25rem, 2.6vw, 1.6rem);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--prism-text);
}

.px-legal .legal-section h3 {
    margin: 30px 0 10px 48px;
    font-size: 0.98rem;
    font-weight: 700;
    color: var(--prism-text);
}

.px-legal .legal-section p,
.px-legal .legal-section ul {
    margin: 0 0 16px 48px;
    font-size: 0.95rem;
    line-height: 1.75;
    color: var(--prism-text-2);
}

.px-legal .legal-section ul {
    padding-left: 20px;
    list-style: none;
}

.px-legal .legal-section li {
    position: relative;
    margin-bottom: 9px;
    padding-left: 4px;
}

.px-legal .legal-section li::before {
    content: '';
    position: absolute;
    top: 0.72em;
    left: -16px;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--prism-accent);
    opacity: 0.75;
}

.px-legal .legal-section a {
    color: var(--prism-accent);
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
}

.px-legal .legal-section a:hover { color: var(--prism-accent-light); }

.px-legal .legal-section strong { color: var(--prism-text); font-weight: 600; }

/* A term the sentence turns on — "read-only", "never" — set as small caps
   rather than bold so a paragraph with three of them still reads as prose. */
.px-legal .legal-emph {
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--prism-text);
}

/* ===== CALLOUT =====
   For the two places the honest answer is a limitation rather than a feature:
   whose certifications those are, and what we have not built yet. Warning-toned
   on purpose — a caveat rendered in the accent colour reads as a brag. */

.px-legal .legal-note {
    margin: 0 0 16px 48px;
    padding: 16px 18px;
    border-radius: 12px;
    border: 1px solid var(--prism-border);
    border-left: 2px solid var(--prism-warning);
    background: var(--prism-warning-wash);
    font-size: 0.89rem;
    line-height: 1.68;
    color: var(--prism-text-2);
}

.px-legal .legal-note strong { color: var(--prism-text); }

/* ===== CONSPICUOUS CLAUSE =====
   For the warranty disclaimer and the liability cap on the terms page, which
   are the two clauses the law expects to be *noticeable* rather than merely
   present — a disclaimer buried in matching body text is the one a court is
   most willing to set aside.

   Convention is to shout them in capitals. This does the job with weight, a
   heavier border and the accent rail instead, because a wall of capitals is
   measurably harder to read and the clause has to be understood, not just
   survived. The <strong> inside carries the operative sentence, so counsel who
   want the traditional all-caps treatment can uppercase that one element
   without touching the layout. */
.px-legal .legal-conspicuous {
    margin: 0 0 16px 48px;
    padding: 18px 20px;
    border: 1px solid var(--prism-border-strong);
    border-left: 3px solid var(--prism-accent);
    border-radius: 12px;
    background: var(--prism-tint-1);
    font-size: 0.9rem;
    line-height: 1.7;
    color: var(--prism-text-2);
}

.px-legal .legal-conspicuous strong {
    color: var(--prism-text);
    font-weight: 700;
}

/* ===== FACT ROWS =====
   The specifics — cipher names, iteration counts, cookie flags. Set as a
   definition list so the claim and its mechanism stay side by side; a reader
   auditing us reads this and skips the prose. */

.px-legal .legal-facts {
    margin: 0 0 20px 48px;
    padding: 4px 0 0;
    border-top: 1px solid var(--prism-border-faint);
}

.px-legal .legal-fact {
    display: grid;
    grid-template-columns: minmax(140px, 200px) minmax(0, 1fr);
    gap: 8px 20px;
    padding: 12px 0;
    border-bottom: 1px solid var(--prism-border-faint);
}

.px-legal .legal-fact-term {
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    color: var(--prism-text-muted);
}

.px-legal .legal-fact-value {
    font-size: 0.88rem;
    line-height: 1.6;
    color: var(--prism-text-2);
}

/* Both places a literal appears — a cookie flag in a paragraph and a cipher in
   a fact row. Bootstrap's stock `code` is #e83e8c, which lands as hot pink
   inside a paragraph of body text; it is only invisible in the app because
   nothing else here sets prose in <code>. */
.px-legal .legal-section code,
.px-legal .legal-fact-value code {
    padding: 1px 6px;
    border-radius: 5px;
    background: var(--prism-tint-2);
    border: 1px solid var(--prism-border-faint);
    font-size: 0.85em;
    color: var(--prism-text);
}

/* ===== CONTACT ===== */

.px-legal .legal-contact {
    margin-top: 54px;
    padding: 32px 30px;
    border-radius: 16px;
    background: var(--prism-tint-1);
    border: 1px solid var(--prism-border);
    text-align: center;
}

.px-legal .legal-contact h2 {
    margin: 0 0 10px;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--prism-text);
}

.px-legal .legal-contact p {
    max-width: 54ch;
    margin: 0 auto 20px;
    font-size: 0.92rem;
    line-height: 1.7;
    color: var(--prism-text-muted);
}

/* The button's label is an email address, and .prism-btn-info uppercases its
   text — which turns a working mailto into JAIMIN@MYPRISM.MONEY, something a
   reader has to squint at to decide whether it is the real address. Case is
   information here, so this is the one button in the app that keeps its own. */
.px-legal .legal-contact .prism-btn-info {
    display: inline-flex;
    align-items: center;
    /* The 1px letter-spacing the button normally carries is what separates its
       icon from its label; dropping it closes the gap too, so it comes back as
       an explicit one. */
    gap: 9px;
    text-transform: none;
    letter-spacing: 0;
}

/* ===== CROSS-LINK TO THE SIBLING DOCUMENT ===== */

.px-legal .legal-siblings {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 46px;
    padding-top: 30px;
    border-top: 1px solid var(--prism-border-faint);
}

.px-legal .legal-sibling {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 11px 20px;
    border-radius: 999px;
    border: 1px solid var(--prism-border);
    background: var(--prism-tint-1);
    font-size: 0.87rem;
    font-weight: 600;
    color: var(--prism-text-2);
    transition: color 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}

.px-legal .legal-sibling:hover,
.px-legal .legal-sibling:focus-visible {
    color: var(--prism-text);
    border-color: var(--prism-accent-border);
    transform: translateY(-1px);
    text-decoration: none;
}

.px-legal .legal-sibling i { color: var(--prism-accent); font-size: 0.9rem; }

/* ===== NARROW ===== */

@media (max-width: 991px) {
    .px-legal .legal-layout { grid-template-columns: minmax(0, 1fr); gap: 34px; }
    /* Un-stick before it can pin a full-height list over a phone viewport. */
    .px-legal .legal-contents { position: static; }
    .px-legal .legal-contents ol {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
        gap: 2px;
    }
}

@media (max-width: 575px) {
    .px-legal .legal-section h3,
    .px-legal .legal-section p,
    .px-legal .legal-section ul,
    .px-legal .legal-note,
    .px-legal .legal-conspicuous,
    .px-legal .legal-facts { margin-left: 0; }
    .px-legal .legal-fact { grid-template-columns: minmax(0, 1fr); gap: 2px; }
    .px-legal .legal-contact { padding: 26px 20px; }
}

/* The contents list is the only motion on these pages and it is a colour
   change, but the transition still needs its own opt-out — there is no global
   reduced-motion block covering it. */
@media (prefers-reduced-motion: reduce) {
    .px-legal .legal-contents a,
    .px-legal .legal-sibling { transition: none; }
    .px-legal .legal-sibling:hover,
    .px-legal .legal-sibling:focus-visible { transform: none; }
}

/* ===== INVITE FORM (public) =====
   Used in: accounts/request-invite.html, scoped by the `px-invite` body class.

   This page is a QUESTIONNAIRE, not an account form, and it has to read as one
   before it is read at all. The first version reused the register page's
   shell -- one card, two fields per row, `.form-control` pills -- and the
   result was indistinguishable from a signup, which is the one thing the beta
   gate exists to say it is not. Somebody arriving from "Request an Invite" and
   seeing a signup form concludes either that they are creating an account or
   that the CTA lied.

   So the vocabulary is the one everybody already reads as "a form somebody
   will read": a narrow single column, one question per card, one question per
   row, a question written as a sentence rather than a field label, underline
   inputs, and real radios instead of a select. Deliberately NOT `.prism-card`
   for the question cards -- that carries a hover lift, and a card that rises
   when the pointer crosses it reads as a control, not a question.

   Everything here is tokens, so it follows all nine skins and both modes with
   no `.white-content` override, and the surfaces stay translucent so the
   ambient ground still shows through (see the design-system note in
   CLAUDE.md -- painting a content surface opaque punches a hole in it). */
.px-invite .invite-page {
    max-width: 660px;
    margin: 0 auto;
    padding-bottom: 40px;
}

.px-invite .invite-card {
    background: linear-gradient(160deg, var(--prism-veil-2) 0%, var(--prism-veil-1) 100%),
                var(--prism-card-bg);
    border: 1px solid var(--prism-border);
    border-radius: var(--prism-radius-panel);
    padding: 22px 26px;
    margin-bottom: 14px;
    position: relative;
}

/* The masthead's colour strip. It is what makes the stack read as one form
   rather than a column of unrelated panels, and it is the accent rather than
   --prism-wordmark on purpose: the wordmark gradient is the brand's identity
   and this is a themeable surface, the same split the design-system note
   draws. Drawn as a pseudo-element so the radius clips it. */
.px-invite .invite-card--head {
    padding-top: 30px;
    overflow: hidden;
}

.px-invite .invite-card--head::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 8px;
    background: linear-gradient(90deg, var(--prism-accent) 0%,
                                rgba(var(--prism-accent2-rgb), 1) 100%);
}

.px-invite .invite-title {
    margin: 0 0 10px;
    font-size: 1.9rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--prism-text);
}

.px-invite .invite-lede {
    margin: 0;
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--prism-text-2);
}

/* Sits under the masthead and again above the button, because on a form this
   short the second one is the only reminder a skimmer gets. */
.px-invite .invite-required {
    margin: 14px 0 0;
    font-size: 0.8rem;
    color: var(--prism-danger);
}

/* The question itself. Regular weight at body size, NOT `.prism-field-label`
   (0.78rem uppercase 700) -- that is a field label on a data-entry screen, and
   it is the single biggest tell that a form is an app form rather than
   something a person will read. */
.px-invite .invite-q {
    display: block;
    margin: 0 0 4px;
    font-size: 1rem;
    font-weight: 500;
    line-height: 1.5;
    color: var(--prism-text);
}

.px-invite .invite-help {
    display: block;
    margin: 0 0 12px;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--prism-text-muted);
}

.px-invite .invite-card textarea.prism-input {
    resize: vertical;
    min-height: 92px;
    line-height: 1.55;
}

/* The select has to lose the platform chrome or it reads as a boxed control
   sitting among underlined ones. */
.px-invite .invite-card select.prism-input {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
}

/* Radios rather than a <select> for the buckets: five options a person is
   meant to consider are a list, and a closed dropdown shows none of them.
   Geometry is `.prism-checkbox`'s, round, with a dot -- including its
   --prism-text-dim ring, which that block measured against the 3:1 WCAG 1.4.11
   wants for a control boundary and which the border-strong alternative failed
   in light mode. */
.px-invite .invite-choices {
    display: grid;
    gap: 10px;
    margin-top: 12px;
}

.px-invite .invite-choice {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    font-size: 0.95rem;
    color: var(--prism-text-2);
}

.px-invite .invite-radio {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    flex: none;
    width: 20px;
    height: 20px;
    border: 2px solid var(--prism-text-dim);
    border-radius: 50%;
    background-color: var(--prism-input-bg);
    cursor: pointer;
    position: relative;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.px-invite .invite-radio:hover {
    border-color: var(--prism-accent-text);
    background-color: var(--prism-tint-4);
}

.px-invite .invite-radio:focus-visible {
    outline: 2px solid var(--prism-accent-text);
    outline-offset: 2px;
}

.px-invite .invite-radio:checked {
    border-color: var(--prism-accent-text);
}

.px-invite .invite-radio:checked::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--prism-accent-text);
}

/* The answer/refusal line. Google Forms puts this above the questions on
   submit; here it is the one place the route's `msg` lands, so it has to carry
   both tones. */
.px-invite .invite-note {
    border-left: 3px solid var(--prism-accent);
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--prism-text-2);
}

.px-invite .invite-note--bad { border-left-color: var(--prism-danger); }
.px-invite .invite-note--good { border-left-color: var(--prism-success); }

.px-invite .invite-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 16px;
    margin-top: 18px;
}

.px-invite .invite-footnote {
    margin: 20px 0 0;
    font-size: 0.82rem;
    line-height: 1.6;
    color: var(--prism-text-muted);
    text-align: center;
}

@media (max-width: 575px) {
    .px-invite .invite-card { padding: 18px 18px; }
    .px-invite .invite-title { font-size: 1.5rem; }
}

/* ===== STATEMENT RETURN SOURCES ===== */
/* Used in: asset-transactions.html (the statement band's "where the return
   came from" track, built by build_asset_statement in asset_statement.py).

   The Total return identity drawn to scale: one track, the negative legs
   (purchase cash, an underwater equity, a negative cash flow) to the LEFT of
   a zero line in the danger colour, the positive legs (what is held, cash
   flow to date, cash pulled out) to its right in their own token colours. Each
   segment's width is its share of the total absolute movement, so the two
   sides read against each other and a loss shows as a longer left. Widths
   arrive as the `--stmt-w` custom property -- data, not style, the same
   channel the score ring's `--perf-score` uses. No text sits on a segment
   (the legend beside it carries the figures), so no colour pair here is
   weighed for contrast. Names deliberately carry no surface word: `-bar`
   is one to the light-mode contract, and a coloured fill is exactly what
   these are. */

.stmt-return-track {
    display: flex;
    align-items: stretch;
    height: 14px;
    border-radius: 7px;
    overflow: hidden;
    background: var(--prism-tint-2);
}

.stmt-return-seg {
    flex: 0 0 auto;
    width: var(--stmt-w, 0%);
    min-width: 2px;
    background: var(--prism-accent);
}

.stmt-return-seg + .stmt-return-seg {
    margin-left: 2px;
}

/* The zero line: money in to its left, money out to its right. */
.stmt-return-zero {
    flex: 0 0 auto;
    width: 2px;
    margin: 0 2px;
    background: var(--prism-text);
}

.stmt-return-seg.is-cash,
.stmt-return-dot.is-cash {
    background: var(--prism-success);
}

.stmt-return-seg.is-refi,
.stmt-return-dot.is-refi {
    background: var(--prism-warning);
}

/* Anything below zero is the danger colour; the cash that went in at the
   start sits lighter than a leg that turned negative since, so the two read
   apart on the same side of the line. */
.stmt-return-seg.is-neg,
.stmt-return-dot.is-neg {
    background: var(--prism-danger);
}

.stmt-return-seg.is-basis,
.stmt-return-dot.is-basis {
    background: var(--prism-danger);
    opacity: 0.6;
}

.stmt-return-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 18px;
    margin-top: 8px;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--prism-text-muted);
    font-variant-numeric: tabular-nums;
}

.stmt-return-legend b {
    color: var(--prism-text);
    font-weight: 700;
}

.stmt-return-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 6px;
    vertical-align: 0;
    background: var(--prism-accent);
}

/* ===== LEDGER ROWS ===== */
/* Used in: exit-planner.html (#exit-ledger, built by exit-planner.js) and
   asset-transactions.html (the Net Cash Flow card's Totals tab).

   A closing-statement column: label left, tabular figure right, itemized
   children indented under their parent, a rule above the total. Written for
   the exit planner and scoped to `px-exit` until the asset page needed the
   same rows for its income / expenses / NOI / debt service / net cash flow
   totals -- two pages declaring one component is the shared sheet's whole
   argument, so the block moved here and lost its scope. The names keep their
   `exit-` prefix: renaming would touch exit-planner.js's renderer and the
   ledger tests that read `.exit-ledger-label` / `.exit-ledger-value`
   positionally, for no gain in behaviour. Nothing here declares a
   background, and nothing may: `ledger` is a surface word to the light-mode
   contract, and these rows sit on whichever card holds them. */

.exit-ledger {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.exit-ledger-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0 16px;
    padding: 7px 0;
    font-size: 0.86rem;
}

.exit-ledger-label {
    color: var(--prism-text-muted);
    font-weight: 500;
}

/* An itemized component — indented child of the row above it, its figure in
   the value column at reading size, the way a closing statement itemizes.
   The label drops weight and the value drops colour so the parents keep the
   visual spine of the statement. */
.exit-ledger-row.is-part {
    padding: 2px 0 2px 16px;
    font-size: 0.8rem;
}

.exit-ledger-row.is-part .exit-ledger-label {
    font-weight: 400;
}

.exit-ledger-row.is-part .exit-ledger-value {
    color: var(--prism-text-muted);
    font-weight: 500;
}

/* Prose under a row, only where the information is not a number: the mode
   that produced the price, the taxes-off disclosure. Full-width third item
   in the wrapped row, so the label/value pair above it keeps one baseline.
   Caption-sized was tried first and could not be read — this is body-muted. */
.exit-ledger-how {
    flex-basis: 100%;
    font-size: 0.76rem;
    line-height: 1.45;
    color: var(--prism-text-muted);
    margin-top: 2px;
}

.exit-ledger-value {
    color: var(--prism-text);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.exit-ledger-row.is-rule {
    border-top: 1px solid var(--prism-border);
    margin-top: 6px;
    padding-top: 12px;
}

.exit-ledger-row.is-total .exit-ledger-label {
    color: var(--prism-text);
    font-weight: 700;
    text-transform: uppercase;
    font-size: 0.72rem;
    letter-spacing: 0.09em;
}

.exit-ledger-row.is-total .exit-ledger-value {
    font-size: 1.35rem;
    font-weight: 700;
}

/* ===== EXIT PLANNER ===== */
/* Used in: exit-planner.html. Scoped to the `px-exit` body class so nothing
   here can reach the rest of the app.

   The page is a closing statement that recalculates. Its spine is a single
   vertical ledger — price at the top, every deduction under it, what is left at
   the bottom — because that is the document a sale actually produces and the
   order an owner already reads it in. The table below is the same statement per
   asset; the charts are the same statement as a shape.

   The one visual idea worth preserving through any restyle is `.exit-assumed`.
   Every figure on this page that rests on a convention we picked rather than on
   something the owner told us carries it. Marking is the whole honesty
   mechanism: a defaulted 20% land split is not a fact, and the alternative to
   marking it is either withholding the number until someone finds 33 tax cards,
   or stating a guess as a measurement. It is drawn in `--prism-accent` and
   deliberately NOT a warning colour — an assumption is not an error, and
   painting the default state of the page amber would make arriving at it feel
   like a fault. */

.px-exit .exit-rail {
    position: sticky;
    top: 0;
    z-index: 12;
    margin-bottom: 22px;
    padding: 14px 20px;
    border: 1px solid var(--prism-accent-border);
    border-radius: 14px;
    /* Translucent plus a blur rather than an opaque slab: the rail floats over
       the table when stuck and has to stay readable, but the ambient mesh still
       has to show through or it reads as a panel pasted on the page. */
    background: var(--prism-overlay);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
}

/* ---- the price, and the answer beside it ---- */

/* One field, always editable, always holding the price the scenario is using.
   The modes in the rail below FILL it rather than replacing it — several ways
   to arrive at a price, one place the price lives. The first version offered
   five modes and no way to type a dollar figure at all, which made the page's
   own headline question unreachable. */
.px-exit .exit-price {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 24px 40px;
    padding: 22px 26px;
    margin-bottom: 18px;
    border-radius: 16px;
}

.px-exit .exit-price-main { flex: 0 1 320px; }

.px-exit .exit-price-label {
    display: block;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--prism-text-muted);
    margin-bottom: 8px;
}

.px-exit .exit-price-input {
    display: flex;
    align-items: baseline;
    gap: 4px;
    border-bottom: 2px solid var(--prism-accent-border);
    transition: border-color 0.2s ease;
}

.px-exit .exit-price-input:focus-within { border-bottom-color: var(--prism-accent); }

.px-exit .exit-price-prefix {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--prism-text-muted);
}

.px-exit .exit-price-input input {
    flex: 1;
    min-width: 0;
    background: none;
    border: 0;
    padding: 2px 0 8px;
    color: var(--prism-text);
    font-size: clamp(1.7rem, 3.2vw, 2.4rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.px-exit .exit-price-input input:focus { outline: none; }

.px-exit .exit-price-rel {
    margin: 8px 0 0;
    font-size: 0.78rem;
}

.px-exit .exit-price-answers {
    display: flex;
    flex-wrap: wrap;
    gap: 14px 36px;
}

.px-exit .exit-price-answer {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.px-exit .exit-price-answer-label {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--prism-text-muted);
}

.px-exit .exit-price-answer-value {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

/* ---- the in-flight state ----
   The results DIM while a recompute is out; they never blank. The last good
   answer stays legible under the dim, which matches the sequence-number
   philosophy in exit-planner.js: a stale figure is closer to the truth than a
   spinner over nothing. The status itself is the shared PrismBusy pill. */

.px-exit .exit-updating {
    opacity: 0.55;
    transition: opacity 0.15s ease;
}

/* ---- the scope bar ----
   One chip per LLC beside the asset dropdown. The chips reuse the toolbar
   button vocabulary (perf-tool-btn / is-active) so scope reads like the mode
   and column switches it sits between. */

.px-exit .exit-scope-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 10px;
}

.px-exit .exit-llc-chips {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
}

/* Card header with a view toggle beside the title (the statement/waterfall
   tabs). */
.px-exit .exit-card-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
}

/* ---- the rail ---- */

.px-exit .exit-rail-head,
.px-exit .exit-rail-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px 18px;
    flex-wrap: wrap;
}

.px-exit .exit-rail-foot {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--prism-border-faint);
}

/* The mode's own input sits beside the buttons that chose it, not in a panel
   somewhere else — it is the thing you reach for next. */
.px-exit .exit-mode-inputs { gap: 14px; }

.px-exit .exit-inline-field {
    display: flex;
    align-items: center;
    gap: 8px;
}

.px-exit .exit-inline-field[hidden] { display: none; }

.px-exit .exit-inline-field .prism-field-label { margin: 0; white-space: nowrap; }

.px-exit .exit-inline-field .form-control { width: 128px; }

.px-exit .exit-inline-field .prism-field-unit { width: 108px; }

/* Three labelled groups, and inside each one every field has the SAME shape:
   a one-line label, then the control. The first version was a nine-cell
   auto-fit grid with `align-items: end` and a different amount of hint text
   under each field — so the boxes aligned at their BOTTOMS and every input
   landed at a different height. Nine fields, nine baselines, and it read as
   broken. The hints are tooltips now for the same reason. */
/* Flex, not an equal-thirds grid: the three groups hold two, three and six
   fields, so equal tracks left Timing two-thirds empty beside a Tax group
   wrapping onto a second row. Each takes what it needs and wraps as a unit. */
.px-exit .exit-rail-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 18px;
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid var(--prism-border-faint);
}

.px-exit .exit-rail-controls[hidden] { display: none; }

.px-exit .exit-group {
    flex: 1 1 auto;
    border: 1px solid var(--prism-border-faint);
    border-radius: 12px;
    padding: 12px 14px 14px;
    margin: 0;
    min-width: 0;
}

.px-exit .exit-group-title {
    float: none;
    width: auto;
    padding: 0 6px;
    margin: 0 0 8px;
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--prism-text-muted);
}

.px-exit .exit-group-fields {
    display: grid;
    /* 132px, not 112: "Ordinary rate" plus its info icon overflowed and the
       label ellipsed to "Ordinary rate …", which is a worse label than a
       narrower box. */
    grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
    /* start, not end: the controls line up under their labels because every
       field is label-then-control and every label is one line. */
    align-items: start;
    gap: 12px;
}

.px-exit .exit-group-fields .prism-field-label {
    display: block;
    margin-bottom: 5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.px-exit .exit-group-fields [hidden] { display: none; }

.px-exit .exit-group-switch { grid-column: 1 / -1; }

.px-exit .exit-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    flex: 1;
}

.px-exit .exit-chips[hidden] { display: none; }

.px-exit .exit-chip {
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--prism-accent-border);
    /* --prism-text on the full tint, not --prism-text-2 on the faintest one:
       the chips are 0.72rem, so they need 4.5:1, and the muted-on-pale pairing
       measured 2.74 against the light-mode ground. */
    background: var(--prism-tint-3);
    color: var(--prism-text);
    font-variant-numeric: tabular-nums;
}

/* The row a value was just typed into. The table re-sorts on every answer, so
   an edited row can land somewhere else entirely — this is what stops the
   number reappearing three rows away with no explanation. */
.px-exit .stats-perf-table tbody tr.is-edited td {
    background: var(--prism-tint-2);
}

/* An editable table cell. Sized to the column rather than to its content so a
   row of them does not ripple as the numbers change. */
.prism-cell-edit { padding-right: 6px; }

.prism-cell-input {
    width: 100%;
    min-width: 90px;
    background: var(--prism-tint-1);
    border: 1px solid var(--prism-accent-border);
    border-radius: 7px;
    padding: 3px 8px;
    color: var(--prism-text);
    font-size: 0.8rem;
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.prism-cell-input:hover { background: var(--prism-tint-2); }

.prism-cell-input:focus {
    outline: none;
    border-color: var(--prism-accent);
    background: var(--prism-tint-2);
}

/* A value the page chose, not one the owner gave it. */
.px-exit .exit-assumed,
.px-exit .stats-perf-bar.is-assumed .stats-perf-cell {
    text-decoration: underline dotted var(--prism-accent);
    text-underline-offset: 3px;
    cursor: help;
}

/* A figure whose provenance is prose — hover or focus it for the shared
   .stats-perf-tip panel. Muted dotting, deliberately NOT the accent dotting
   .exit-assumed uses one rule up: that one marks a number the page GUESSED,
   this one marks a number it can explain, and two different claims must not
   wear the same mark. Without any mark at all the panel is simply undiscovered,
   which is what a `title` attribute at least had the OS to hint at. */
.px-exit .exit-ledger-label[data-tip],
.px-exit .exit-price-answer[data-tip] .exit-price-answer-label {
    text-decoration: underline dotted var(--prism-text-dim);
    text-underline-offset: 3px;
}

/* The mark goes on the LABEL, the cursor on the whole target: text-decoration
   propagates to descendants, so marking the wrapper drew a dotted rule under a
   1.5rem figure as well — which reads as a link, not as a hint. */
.px-exit .exit-ledger-label[data-tip],
.px-exit .exit-price-answer[data-tip] {
    cursor: help;
}

/* They are tabbable now — `title` never was — so the keyboard path needs to be
   visible. Matches .aperf-info's focus treatment. */
.px-exit .exit-ledger-label[data-tip]:focus-visible,
.px-exit .exit-price-answer[data-tip]:focus-visible {
    outline: 2px solid var(--prism-accent);
    outline-offset: 2px;
    border-radius: 4px;
}

.px-exit .prism-field-derived {
    border-bottom-style: dashed !important;
}

/* ---- the curve, which is also the price control ---- */

.px-exit .exit-curve-hint {
    font-size: 0.76rem;
    color: var(--prism-text-muted);
    margin: 0 0 10px;
}

.px-exit .exit-curve-hint[hidden] { display: none; }

/* ---- disclosures ---- */

.px-exit .exit-note {
    font-size: 0.78rem;
    line-height: 1.6;
    color: var(--prism-text-muted);
    margin: 14px 0 0;
}

.px-exit .exit-note[hidden] { display: none; }

.px-exit .exit-note strong { color: var(--prism-text-2); font-weight: 600; }

.px-exit .exit-empty {
    padding: 42px 20px;
    text-align: center;
    color: var(--prism-text-muted);
}

.px-exit .exit-empty[hidden] { display: none; }

@media (max-width: 767px) {
    .px-exit .exit-rail { position: static; padding: 14px; }
}

/* ===== IMPORT WIZARD =====
   Used in: import-data.html. Scoped to the px-import body class so nothing
   here leaks into the rest of the app. The step rail, fields and segmented
   buttons are the shared kits above (FORM WIZARD, FORM FIELD VOCABULARY,
   ASSET PERFORMANCE's perf-tool-btn); this section is only the surfaces
   those arrange: source cards, the drop zone, the decision rows, the review
   stats and the batch list. Tokens only — no mode-specific literals. */

.px-import .import-card { max-width: 1040px; margin: 0 auto 24px; }
.px-import .import-batches { max-width: 1040px; margin: 0 auto; }

.px-import .import-source-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
    margin-bottom: 8px;
}

.px-import .import-source-card {
    display: flex;
    flex-direction: column;
    gap: 5px;
    align-items: flex-start;
    padding: 12px 14px;
    border: 1px solid var(--prism-border);
    border-radius: 10px;
    background: var(--prism-surface-2);
    color: var(--prism-text);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.px-import .import-source-card:hover { border-color: var(--prism-accent-border); }

.px-import .import-source-card.is-active {
    border-color: var(--prism-accent-text);
    background: var(--prism-tint-3);
    box-shadow: 0 0 0 3px rgba(var(--prism-accent-rgb), 0.10);
}

.px-import .import-source-name { font-weight: 700; font-size: 0.95rem; }

.px-import .import-source-hint {
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
}

.px-import .import-source-note { margin: 0 0 16px; }

.px-import .import-target-row,
.px-import .import-option-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 14px;
}

.px-import .import-target-row[hidden],
.px-import .import-option-row[hidden] { display: none; }

.px-import .import-account-pick[hidden] { display: none; }

.px-import .import-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 180px;
    padding: 26px 20px;
    border: 2px dashed var(--prism-border);
    border-radius: 14px;
    background: var(--prism-surface-2);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.px-import .import-drop:hover,
.px-import .import-drop.is-over {
    border-color: var(--prism-accent-text);
    background: var(--prism-tint-2);
}

.px-import .import-drop.has-file { border-style: solid; border-color: var(--prism-accent-border); }

.px-import .import-drop > i {
    font-size: 30px;
    color: var(--prism-accent-text);
}

.px-import .import-drop-title { font-weight: 700; color: var(--prism-text); }

.px-import .import-drop-sub {
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
    max-width: 460px;
}

.px-import .import-recognized {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 11px 14px;
    margin-bottom: 18px;
    border: 1px solid var(--prism-accent-border);
    border-radius: 10px;
    background: var(--prism-tint-2);
    color: var(--prism-text);
    font-size: 0.9rem;
}

.px-import .import-recognized:empty { display: none; }

.px-import .import-recognized i { color: var(--prism-accent-text); }

.px-import .import-map-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 12px 16px;
    margin-bottom: 18px;
}

.px-import .import-block-title {
    margin: 20px 0 10px;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--prism-text);
}

.px-import .import-bank-target {
    padding: 12px 14px;
    border: 1px solid var(--prism-border);
    border-radius: 10px;
    background: var(--prism-surface-2);
    color: var(--prism-text-2);
}

.px-import .import-bank-target[hidden] { display: none; }

.px-import .import-decision-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 8px;
    border: 1px solid var(--prism-border);
    border-radius: 10px;
    background: var(--prism-surface-2);
}

.px-import .import-decision-raw {
    font-weight: 600;
    color: var(--prism-text);
    min-width: 140px;
}

.px-import .import-decision-arrow { color: var(--prism-text-muted); }

.px-import .import-decision-pick {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.px-import .import-or {
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
}

.px-import .import-pill {
    padding: 3px 9px;
    border-radius: 999px;
    font-size: var(--prism-fs-xs);
    font-weight: 700;
    letter-spacing: 0.3px;
    color: var(--prism-accent-text);
    background: var(--prism-tint-3);
    border: 1px solid var(--prism-accent-border);
}

.px-import .import-matched { margin: 6px 0 12px; }

.px-import .import-matched[hidden] { display: none; }

.px-import .import-matched > summary {
    cursor: pointer;
    color: var(--prism-text-muted);
    font-size: 0.85rem;
}

.px-import .import-matched-row {
    padding: 5px 2px;
    font-size: 0.85rem;
    color: var(--prism-text-2);
}

.px-import .import-matched-row i { color: var(--prism-text-muted); margin: 0 4px; }

.px-import .import-default-asset { margin: 10px 0 4px; }

.px-import .import-default-asset[hidden] { display: none; }

.px-import .import-default-asset-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}

.px-import .import-default-asset-new { max-width: 220px; }

.px-import .import-summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
    margin-bottom: 16px;
}

.px-import .import-stat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 12px 14px;
    border: 1px solid var(--prism-border);
    border-radius: 10px;
    background: var(--prism-surface-2);
}

.px-import .import-stat-value {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--prism-text);
}

.px-import .import-stat-label {
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
}

.px-import .import-check {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    color: var(--prism-text-2);
    font-size: 0.9rem;
    cursor: pointer;
}

.px-import .import-check[hidden] { display: none; }

.px-import .import-sample-wrap {
    overflow-x: auto;
    border: 1px solid var(--prism-border);
    border-radius: 10px;
}

.px-import .import-sample {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.px-import .import-sample th,
.px-import .import-sample td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid var(--prism-border);
    color: var(--prism-text-2);
    white-space: nowrap;
}

.px-import .import-sample th {
    font-size: var(--prism-fs-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    color: var(--prism-text-muted);
}

.px-import .import-sample tr:last-child td { border-bottom: 0; }

.px-import .import-sample .num { text-align: right; }

/* The display contract: direction comes from flow_direction, never a sign. */
.px-import .import-sample .is-inflow { color: var(--prism-success); }
.px-import .import-sample .is-outflow { color: var(--prism-danger); }

.px-import .import-sample tr.is-duplicate td { opacity: 0.55; }

.px-import .import-blocker { margin: 0 auto 0 0; max-width: 60%; }

.px-import .import-done {
    text-align: center;
    padding: 26px 16px 10px;
}

.px-import .import-done > i {
    font-size: 40px;
    color: var(--prism-success);
    margin-bottom: 10px;
}

.px-import .import-done h3 { margin: 8px 0 6px; color: var(--prism-text); }

.px-import .import-done p { color: var(--prism-text-muted); margin-bottom: 16px; }

.px-import .import-done-links {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
    padding-bottom: 14px;
}

.px-import .import-batch-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 9px 4px;
    border-bottom: 1px solid var(--prism-border);
}

.px-import .import-batch-row:last-child { border-bottom: 0; }

.px-import .import-batch-row.is-reverted { opacity: 0.55; }

.px-import .import-batch-name { font-weight: 600; color: var(--prism-text); }

.px-import .import-batch-meta {
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-muted);
    margin-right: auto;
}

@media (max-width: 767px) {
    .px-import .import-decision-raw { min-width: 0; width: 100%; }
    .px-import .import-blocker { max-width: 100%; }
}


/* ===== NOTIFICATIONS PAGE =====
   Used in: app/home/templates/notifications.html.

   The rows reuse the .tx-list-* vocabulary wholesale -- container, date group,
   day label, hover, row borders -- and override only the grid, which is the
   same layering admin.css's .adm-row--* classes do. .tx-list-row's eight
   columns are the transactions ledger's and mean nothing here. */
.ntf-row {
    /* icon | message + time | status | actions */
    grid-template-columns: 28px minmax(0, 1fr) 92px auto;
    cursor: default;
}

/* Unread is an accent bar down the leading edge plus the faintest fill. An
   inset box-shadow rather than a border-left so the row's own box, and the
   grid tracks above, do not shift by 3px when a row is marked read. */
.ntf-row.is-unread {
    background: var(--prism-tint-1);
    box-shadow: inset 3px 0 0 var(--prism-accent);
}

.ntf-row.is-unread:hover {
    background: var(--prism-tint-3);
}

.ntf-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
}

.ntf-body {
    min-width: 0;
}

.ntf-msg {
    font-size: var(--prism-fs-base);
    line-height: 1.4;
    color: var(--prism-text);
    word-break: break-word;
}

.ntf-row:not(.is-unread) .ntf-msg {
    color: var(--prism-text-2);
}

.ntf-msg a {
    color: var(--prism-accent-text);
    text-decoration: none;
}

.ntf-msg a:hover {
    text-decoration: underline;
}

.ntf-meta {
    display: block;
    margin-top: 3px;
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-dim);
    font-variant-numeric: tabular-nums;
}

.ntf-status {
    display: flex;
    align-items: center;
}

.ntf-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 2px;
}

/* No `[hidden]` guards here, and that is checked rather than assumed:
   black-dashboard.css ships `[hidden] { display: none !important }` at author
   level (line 389, Bootstrap's reboot), and layouts/base.html loads it on every
   page ahead of this sheet. So it outranks the `display` any class here sets --
   measured in the browser on a `.prism-action-btn` and a `.prism-badge-warning`
   carrying the attribute and no guard: both compute `display: none`. The
   familiar trap is the UA-level `[hidden]`, which does lose to a class
   selector; that is not the rule in play in this app. */

/* .prism-search-input is width:100%, which inside a flex toolbar group claims
   the whole row. Sized rather than grown: letting it flex made the group's
   max-content width collapse and wrapped the Clear button onto its own line. */
.ntf-search {
    width: 320px;
    max-width: 100%;
    flex: 0 1 320px;
}

/* The filter group is the one that should absorb the toolbar's slack; without
   this both groups sit at content width and the row wraps early. */
.ntf-toolbar > .perf-toolbar-group:first-child {
    flex: 1 1 auto;
}

.ntf-note {
    padding: 10px 4px 0;
    font-size: var(--prism-fs-xs);
    color: var(--prism-text-dim);
}

/* The list is the page, so let it use the page's height rather than the
   ledger's viewport-relative cap, which leaves a scroller inside a scroller. */
.ntf-list.tx-list-container {
    max-height: none;
    overflow: visible;
}

@media (max-width: 768px) {
    /* Status is already said by the accent bar and by which action button is
       offered, so it is the column to lose first. */
    .ntf-row {
        grid-template-columns: 24px minmax(0, 1fr) auto;
    }

    .ntf-status {
        display: none;
    }
}
