body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background-color: var(--bg-obsidian);
    color: var(--text-main);
    letter-spacing: -0.01em;
    margin: 0;
    padding: 0;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
 
.serif { font-family: 'Libre Baskerville', serif; letter-spacing: 0.03em; }
.overflow-wrap-anywhere { overflow-wrap: anywhere; word-break: break-word; }
.mono { font-family: 'JetBrains Mono', monospace; }
 
/* Display Management */
.hidden-view { display: none !important; }

/* Boot splash (fills the auth-resolve window so it's never a blank screen) */
@keyframes bootspin { to { transform: rotate(360deg); } }
.boot-spinner { width: 34px; height: 34px; border: 3px solid var(--border-silver); border-top-color: var(--foundation-blue); border-radius: 50%; animation: bootspin 0.8s linear infinite; }
/* LOG OFF label: icon-only on mobile (nav is full), icon + label on desktop */
.logoff-label { display: none; }
/* Fixed width so the button is identical in EN (LOG OFF) and ES (CERRAR) — sized to the longer, LOG OFF. */
@media (min-width: 1024px) { .logoff-label { display: inline-block; width: 64px; text-align: left; white-space: nowrap; } }
/* Mobile: fund name at 18px, letter-spacing 0 so the name's WIDTH lines up with the FOUNDING MEMBER
   PORTAL subtitle beneath it (the two edges sync). Size dropped one notch so it still fits two lines
   next to the header buttons; desktop (lg:) untouched. NOTE: adding tracking here widens the name and
   breaks that width-sync, so it stays 0 even though the login name carries 0.05em. */
@media (max-width: 1023px) {
    #nav-fund-title { font-size: 1rem; letter-spacing: 0; line-height: 1.12; }
    #gate-title { letter-spacing: 0; }   /* match the app name (0) so login == app on mobile */
}

/* 3D Flip Card Architecture */
.flip-container { perspective: 1000px; cursor: pointer; height: 100%; width: 100%; }
.flip-container.flipped .flipper { transform: rotateY(180deg); }
/* .flip-container sets height:100% unlayered and beat the h-[500px]/h-[552px] utilities, so the
   funded container filled #view-dashboard and the intent one was pushed past the fold. Id wins.
   Mobile keeps @media(max-width:1023px) .flip-container{height:480px!important}. */
/* Marquee card — head of the left column, matching the line chart: full column width and the same
   52px the chart's own title row (.chart-head) uses. 16px padding all round; the 16px gap below is
   the stack's existing space-y-4. Desktop only — the mobile marquee is the separate #nav-marquee-wrap
   in the header and is untouched. */
@media (min-width: 1024px) {
    /* Horizontal padding only. The card is a fixed 52 with align-items:center, so vertical padding
       just squeezes the content box (52 - 32 - 2 = 18) and leaves the glyphs to sit wherever the
       line box lands. line-height:1 collapses that box onto the glyphs so flex centres the text
       itself rather than the leading around it. */
    #mod-marquee { height: 52px; padding: 0 16px; }
    #mod-marquee #nav-marquee-desktop { line-height: 1; }
}

/* The four bottom-left engine cards get the portal's 52px head. Seven bars in all: a front on each
   of the four, plus the back on the three that flip (quick-calc has no flip, and its NAV/STATUS
   strip is p-0 with no h3, so the > h3 child selector skips it).
   These shipped as an inline h3 with mb-2/pb-2/border-b inside a px-4 py-2 card — about a 24px band
   with the rule line inset 16px each side. The portal's heads are a full-bleed 52. margin:0 -16px
   pulls the border out to the card edges; padding:0 16px puts the text back where it was.
   padding-top:0 on the card is what makes it a real 52 — otherwise the card's py-2 sits above the
   band and the head measures 60 from the card edge. Unlayered here, so it beats the px-4 py-2 and
   mb-2 utilities that live in @layer utilities under v4. Desktop only; below 1024 the cards are
   h-auto and the engine's own spacing applies. */
@media (min-width: 1024px) {
    #mod-treasury-matrix .blueprint-card > h3,
    #mod-settlement-routing .blueprint-card > h3,
    #mod-chart-ownership .blueprint-card > h3,
    #mod-quick-calc .blueprint-card > h3 {
        height: 52px;
        flex-shrink: 0;
        display: flex;
        align-items: center;
        margin: 0 -16px;
        padding: 0 16px;
    }
    #mod-treasury-matrix .blueprint-card,
    #mod-settlement-routing .blueprint-card,
    #mod-chart-ownership .blueprint-card,
    #mod-quick-calc .blueprint-card { padding-top: 0; }

    /* 268, not 290: 22 off each of the two bottom rows. Measured from bottoms, not heights — the two
       columns do NOT start at the same y (left column top 100, .kpi-pillar top 168), so comparing
       1180 against 1068 was the wrong comparison and gave 112. The left block starts at 684 and the
       pillar ends at 1236, so the block has to be 552: 268 + 16 + 268. 684 + 552 = 1236.
       No min-height, here or in the markup: the whole column is hidden lg:block so it never renders
       below 1024, which made the min-h-[290px] these shipped with pure dead weight that could only
       ever override the height and pin the cards open.
       Valuation Tool is not listed because it is flex-1 inside #mod-quick-calc: 268 - 16 - 74 = 178
       falls out on its own. */
    #mod-treasury-matrix,
    #mod-settlement-routing,
    #mod-chart-ownership,
    #mod-quick-calc { height: 268px; }

    /* Valuation Tool declared, not negotiated. It shipped as flex-1, which is flex:1 1 0% and still
       carries min-height:auto — so it only surrendered as much of the 22px cut as its own content
       allowed, kept the rest, and pushed the NAV/STATUS strip below 1236 (#mod-quick-calc has no
       overflow-hidden, so the excess shows rather than clips). flex:none is 0 0 auto: no grow, no
       shrink, no negotiation. 178 = 268 - 16 gap - 74 strip. If the strip's 74 ever changes, this
       number has to change with it. */
    #mod-quick-calc > .blueprint-card:first-child { height: 178px; flex: none; }

    /* Valuation Tool: equal air around the input — 4 above, 4 below.
       Above replaces the label's mb-1.5 (6px). #mod-quick-calc holds exactly one label; the other
       mb-1.5 labels on the page are in the intent form, a different subtree.
       Below needs BOTH margins zeroed. Tailwind v4's space-y-3 puts 12px on the first child's
       margin-BOTTOM as well as the second child's margin-TOP, so overriding only margin-top left
       12 + 4 = 16 and the gap stayed visibly bigger than the 4 above. Measured, not reasoned:
       div1 came back mb12px. Also kills pt-3's padding and mt-auto's slack on the second child. */
    /* Top text sits 8 under the head, the same 8 the ownership legend gets from the donut grid's
       padding-top. justify-center was splitting the card's leftover 12px as 6 above / 6 below, so
       the first line sat 6 down instead of 8 and the spacing moved whenever the content did.
       flex-start + a declared 8 pins it; whatever slack is left falls below the money line. */
    #mod-quick-calc .space-y-3 { justify-content: flex-start; padding-top: 8px; }
    /* The gaps absorb the slack, never the input — h-[32px] is the house field height (82 uses
       file-wide, every field in Members and Operations), so it does not move.
       Wrapper is 116 (178 card - 2 borders - 8 card pad - 52 head). Fixed content is 92: label 16,
       input 32, EST 16, money 28. Minus the 8 top pad that leaves 16 for three gaps. 16/3 is not
       whole, so label->input carries the odd pixel at 6 — furthest from the EST/money pair, where
       an uneven gap would read as a broken pair. 8 + 92 + 16 = 116 exactly, so nothing is left for
       justify to distribute and the only air under the money line is the card's own 8. */
    #mod-quick-calc label { margin-bottom: 6px; }
    #mod-quick-calc .space-y-3 > * { margin-bottom: 0; padding-top: 0; }
    #mod-quick-calc .space-y-3 > * + * { margin-top: 5px; }
    #mod-quick-calc .space-y-3 > * + * > *:first-child { margin-bottom: 5px; }

    /* Donut wells, declared. 268 - 2 borders - 8 card pad-bottom = 258 content; 258 - 52 head = 206
       grid; 206 - 8 top pad = 198 donut, and the card's own 8 supplies the space underneath, so the
       chart sits on 8 above and 8 below. Replaces the pt-1 (4px) the grid shipped with.
       flex:none and margin:0 kill the flex-1/my-auto negotiation — the grid is 206 because it is
       written as 206, not because it happened to fill. */
    #mod-treasury-matrix .front > .grid,
    #mod-chart-ownership .front > .grid {
        height: 206px;
        flex: none;
        margin: 0;
        padding-top: 8px;
        padding-bottom: 0;
    }
    #mod-treasury-matrix .front .relative,
    #mod-chart-ownership .front .relative { height: 198px; }

}

/* Tracker-card footer line centres in its box. It is the last child of a flex-col card, so any
   height it ends up with beyond the text was sitting under the line. Desktop only — the whole
   element is display:none below 1024. */
@media (min-width: 1024px) {
    /* overflow:hidden makes this the scroll window for #kpi-disclaimer-text, which now carries
       .css-marquee — rafMarqueeTick measures el.parentElement as the wrap, and that is this div. */
    /* Fixed box, no vertical padding. With align-items:center, padding cannot move the text — it
       only shrinks the box the text centres in, which is why 12/12 and 11/12 both looked off.
       44 is what the box already measured (1 border + 11 + 19 + 12 = 43), so nothing above it moves.
       line-height:1 collapses the span's line box (leading-[1.65] on the console copy's 12px = ~19.8)
       onto the glyphs, so flex centres the text itself rather than the leading around it.
       34px, not 44: #tracker-card is h-[348px] + overflow-hidden, so its content box is 346 and the
       children must total that. tracker-head 52 + tracker-kpi-body 214 + bars row 46 leaves exactly
       34 for this strip. At 44 the children summed 356 and the card silently CLIPPED the bottom 10px
       of this element — the text centred correctly in a 44 box whose lower half was never painted,
       which is why every rect measured symmetric while the line looked high on screen. Any change to
       the three siblings above has to come back out of this number.
       padding-bottom 3: align-items:center centres the FONT box, and JetBrains Mono hangs 3.6px of
       descender below the baseline at 12px, so a centred font box puts the ink block ~1.5px low. */
    /* line-height:1 lives on the parent, not the span: the scroll-window wrapper is a block and
       would otherwise inherit leading-[1.65], giving it a ~16.5px strut that grows the wrapper and
       pushes the text off centre in a 34px strip. Inherited, so the console copy's own
       font-size:12px span resolves to a 12px line box. Beats the leading-[1.65] utility because
       styles.css is unlayered and Tailwind v4 utilities sit in @layer utilities. */
    .kpi-disclaimer { display: flex; align-items: center; overflow: hidden; height: 34px; padding-top: 0; padding-bottom: 3px; line-height: 1; }
    .kpi-disclaimer #kpi-disclaimer-text { line-height: 1; }

}

#funded-flip-container { height: 500px; }
#intent-flip-container { height: 552px; }

/* #intent-chart-view is a SIBLING of .flipper now (index.html), not a child of .back. Both sit in
   one grid cell by default, so the chart covers the card when hidden-view comes off — which is what
   flipping to it used to look like, and is why stacked and the console are unaffected. The unstacked
   rule below is the only thing that gives it a column of its own. */
#intent-flip-container { display: grid; }
#intent-flip-container > .flipper,
#intent-flip-container > #intent-chart-view { grid-area: 1 / 1; }

.flipper { transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1); transform-style: preserve-3d; height: 100%; width: 100%; display: grid; min-height: 0; }
.front, .back { backface-visibility: hidden; width: 100%; height: 100%; grid-area: 1 / 1; min-height: 0; }
.front { transform: rotateY(0deg); z-index: 2; pointer-events: auto; }
.back { transform: rotateY(180deg); z-index: 1; pointer-events: none; }
.flip-container.flipped .front { z-index: 1; pointer-events: none; }
.flip-container.flipped .back { z-index: 2; pointer-events: auto; }

/* ── TRACKER, UNSTACKED ONLY: table left, chart right ────────────────────────────────────────────
 *
 * .flipper is ALREADY a grid — .front and .back both sit at grid-area 1/1, one cell, separated only
 * by a rotateY. Side by side is that same grid given a second column with the rotation cancelled. No
 * DOM move, no second canvas, no JS: the charts already exist (#funded-chart-canvas and
 * #intent-chart-canvas) and are already drawn, they were just behind something.
 *
 * TWO CONSTRAINTS, BOTH SATISFIED BY THE SELECTOR RATHER THAN BY A FLAG:
 *
 *   1. Stacked must not change. 1812 is the `wide` breakpoint (build/tailwind-input.css), so 1024
 *      through 1811 keeps the flip exactly as it was.
 *      body:not(.force-stack) IS NOT OPTIONAL. Width is not what decides stacked — the masthead
 *      double-tap sets body.force-stack and stacks the portal at ANY width, 2560 included
 *      (styles.css:1959). A bare `min-width: 1812px` is therefore true in stacked mode and this
 *      whole block fired there, which is exactly what it was written not to do. Any rule keyed to
 *      "we are unstacked" needs both halves: the media query AND the class.
 *   2. The console must not change — and this is the important one. _mountTracker()
 *      (index.html:8383) does not copy these cards, it MOVES them: appendChild pulls
 *      #funded-flip-container and #intent-flip-container out of #view-dashboard and into the
 *      console's #rback-fund / #rback-int panes. Rooting this rule at #view-dashboard means it
 *      STOPS MATCHING the instant they are mounted into the console, and starts again when they are
 *      handed back. It disables itself. There is no flag to keep in sync and nothing to remember —
 *      which matters here, because that mount/unmount pair already has a documented footgun: leave
 *      the console before its 650ms timeout and the portal's dashboard has no tracker cards at all.
 *      Do not re-root this selector at .flip-container or at body.
 *
 * Specificity: #view-dashboard .flip-container .flipper is (1,2,0) and beats
 * .flip-container.flipped .flipper at (0,3,0), so `.flipped` becomes a no-op here without !important
 * — the class may still be toggled by the console path, it simply has nothing to turn.
 *
 * FUNDED ONLY. Scoped to #funded-flip-container, not .flip-container, because funded is the only
 * card whose two faces ARE the two cells — table and chart, the same data twice. #intent-flip-container
 * flips personal against ENTITY, which is two different datasets, and splitting it put them side by
 * side and left the 52px bar toggling a class with nothing to turn. Intent keeps its flip at every
 * width; its chart belongs in the second column as its own element, not as a face of that card.
 *
 * Each cell is (1098 - 16) / 2 = 541. The tables measured 432 of 1098 for their headers, so headers
 * fit with room; the BODY is unmeasured and is the thing likely to bite — VILLAVICENCIO at 13
 * characters, and SUBMITTED carrying a full "06/05/2026 09:02 AM" datetime. */
@media (min-width: 1812px) {
    body:not(.force-stack) #view-dashboard #funded-flip-container .flipper {
        grid-template-columns: 1fr 1fr;
        gap: 1rem;
        transform: none;
    }
    body:not(.force-stack) #view-dashboard #funded-flip-container .front,
    body:not(.force-stack) #view-dashboard #funded-flip-container .back {
        transform: none;
        backface-visibility: visible;
        pointer-events: auto;
        z-index: auto;
    }
    body:not(.force-stack) #view-dashboard #funded-flip-container .front { grid-area: 1 / 1; }
    body:not(.force-stack) #view-dashboard #funded-flip-container .back  { grid-area: 1 / 2; }

    /* INTENT: the card splits, the FLIPPER does not. .flipper keeps both faces in column 1 and goes
       on flipping personal against entity exactly as it does stacked — the 52px bar is unchanged at
       every width. The chart takes column 2 as its own element and is always on, which is why it
       has to beat .hidden-view here: that class is the state the console and stacked still use to
       decide whether the chart is showing, and both must keep working. */
    body:not(.force-stack) #view-dashboard #intent-flip-container {
        grid-template-columns: 1fr 1fr;
        gap: 1rem;
    }
    body:not(.force-stack) #view-dashboard #intent-flip-container > .flipper { grid-area: 1 / 1; }
    body:not(.force-stack) #view-dashboard #intent-flip-container > #intent-chart-view {
        grid-area: 1 / 2;
        /* !important is required, not decoration: .hidden-view is `display:none !important`
           (styles.css:21) and that is the class the console and stacked still toggle to decide
           whether the chart is showing. Two !important declarations are settled by specificity, and
           this selector carries three ids against that rule's one class. Without it the chart stays
           display:none in column 2 and the row reads as table-or-chart instead of both. */
        display: flex !important;
    }
}
/* Roster area = plain 2D show/hide (NO outer 3D flip — nesting a 3D flip inside a 3D flip hides the inner
   fronts). Admin roster (rface-front) swaps with the member tracker pair (rface-back); the INNER funded/intent
   cards keep the portal's own 3D flip for tables↔charts, untouched. */
@keyframes rfaceFade { 0% { opacity: 0; } 100% { opacity: 1; } }
#roster-flip > .rface { position: absolute; inset: 0; animation: rfaceFade 0.2s ease-out; }
#roster-flip > #rface-back { display: none; gap: 12px; }

/* ── CONSOLE-ONLY TRACKER OVERRIDES ────────────────────────────────────────────────────────────
   _mountTracker() MOVES the portal's #funded-flip-container / #intent-flip-container into these two
   panes — same nodes, not copies — so every portal rule follows them in. Scoped to #rback-int /
   #rback-fund: those selectors cannot match while the tables are in the portal, so the portal is
   untouched by construction. Tune freely in here.

   Read straight off the metrics grid above. Table 92..941 (849px); the outer edges already agree
   (table left 92 vs grid left 93, table right 941 = the KPI block's right edge), so only the
   interior moves:
       col 4 IS the ENTITY KPI          536.0 .. 643.4   12.6%
       col 5 content starts on the FUNDED AMOUNT KPI     766.8
   Col 4 being exactly one KPI wide is what lets STATUS stay centred the way it is everywhere else:
   the pill fills the column at 100%, and the centred header centres on the same axis.
   The 123.4px between the two KPIs is not a fudge — it is the OPEN/FUNDED card (107.4) plus its two
   8px gaps — and it lives as padding on col 5 rather than as extra width on col 4. Expressed as
   14.54%: percentage padding on a table cell resolves against the TABLE, so 14.54% of 849 = 123.4. */
#rback-int col:nth-child(1) { width:  6%    !important; }
#rback-int col:nth-child(2) { width: 20%    !important; }
#rback-int col:nth-child(3) { width: 26.3%  !important; }   /* -> col 4 starts at 52.30% */
#rback-int col:nth-child(4) { width: 12.6%  !important; }   /* = ENTITY KPI */
#rback-int col:nth-child(5) { width: 20.77% !important; }
#rback-int col:nth-child(6) { width: 14.33% !important; }
/* STATUS: pill fills its column, so it IS the KPI card. Header stays centred — same axis. */
#rback-int .th-count,
#rback-int .tracker-body td:nth-child(4) > span { width: 100% !important; }
#rback-int .th-count-cell,
#rback-int .tracker-body th:nth-child(4),
#rback-int .tracker-body td:nth-child(4) { padding-left: 0; padding-right: 0; }
/* OMNI + SUBMITTED start on the FUNDED KPI's left edge (651.4), not the AMOUNT KPI's — pinning the
   search to the AMOUNT KPI made it 174px with nothing around it, which read as cramped. Col 4 ends
   at 643.0 and the FUNDED card starts at 651.4, so the padding here is just the metrics grid's own
   8px gap between cards: 8.4 / 849 = 0.99%. That leaves the cell 289.6px, wide enough for the omni
   to keep the app-wide 188px it shares with the roster, verification and activity searches — no
   width override here, the inline 188px stands. */
#rback-int .th-search-cell,
#rback-int .tracker-body th:nth-child(5),
#rback-int .tracker-body td:nth-child(5) { padding-left: 0.99%; padding-right: 0; }
/* ST stays CENTRED, exactly as the portal has it, and still ends where the omni ends. That works
   the same way the portal's does: the trailing gap is centring slack plus padding, not padding
   alone. Body text is 14.4px in a 25.7px box, so 5.7px of slack falls after it; 11.30% (95.9px)
   of padding makes up the rest of the omni's 101.6px tail. The header overshoots by ~4px because
   the sort arrow is centred with the label — same as it does in the portal.
   !important is load-bearing, not decoration. The ST cells carry an inline
   padding-right:calc(33% - 204px) (index.html th :3244, td :7144) which is the PORTAL's tail, and an
   inline attribute beats any unflagged author rule — so without the flag this whole block silently
   did nothing and ST sat on 76.2px instead of 95.9px, ~10px past the omni's right edge. */
#rback-int .tracker-body th:nth-child(6),
#rback-int .tracker-body td:nth-child(6) { padding-left: 0 !important; padding-right: 11.30% !important; }
/* Col 1 keeps the portal's left alignment but on 16px, not 15: the metrics card and the roster
   card share a left edge (both 91.5), and the METRICS chevron sits on that card's own 16px header
   inset. Measured — chevron glyph 107.5, # glyph and dot were 106.5. Same 16 puts all three on
   107.5. This is copying the header's value, not tuning a number. */
#rback-int .tracker-body th:nth-child(1),
#rback-int .tracker-body td:nth-child(1),
#rback-int .th-dot-cell { padding-left: 16px; }

/* ── RIGHT PANE (#rback-fund, FUNDED) ──────────────────────────────────────────────────────────
   Table 954.5 .. 1803.5, the same 849px as the left. Anchors, measured off the metrics row:
       count badge right edge  = SCROLL select right  1539.2  -> col 4 ends at 68.87%
       omni + CONFIRMED start  = VIDEO select left    1549.1  -> col 5 padding-left 1.17%
   Col 4 is the badge's own 105px, so the badge fills it at 100% and the AMOUNT under it — which the
   portal renders text-right — lands its .00 on the badge's right edge with no padding. Same
   construction as the portal's funded column, different anchor.
   ST repeats the left pane: ends with the omni (1737.1), leftover as a tail, and it stays CENTRED —
   5.66px of centring slack plus 7.15% of padding makes up the 66.4px gap.                        */
#rback-fund col:nth-child(1) { width:  6%    !important; }
#rback-fund col:nth-child(2) { width: 20%    !important; }
#rback-fund col:nth-child(3) { width: 30.5%  !important; }   /* money badge keeps its 16px off col 3's start, which does not move */
#rback-fund col:nth-child(4) { width: 12.37% !important; }   /* = the 105px count badge */
#rback-fund col:nth-child(5) { width: 20.95% !important; }
#rback-fund col:nth-child(6) { width: 10.18% !important; }
/* Col 1 on the same 16px as the left table, off this table's own left edge. */
#rback-fund .tracker-body th:nth-child(1),
#rback-fund .tracker-body td:nth-child(1),
#rback-fund .th-dot-cell { padding-left: 16px; }
/* AMOUNT: badge fills col 4; the .00 ends on it. The portal's 4px tail is not wanted here — there
   the badge was 105 in a 109 column, here the column IS 105. */
#rback-fund .th-count { width: 100% !important; }
#rback-fund .th-count-cell,
#rback-fund .tracker-body th:nth-child(4),
#rback-fund #funded-table-body td:nth-child(4) { padding-left: 0; padding-right: 0; }
/* OMNI + CONFIRMED start on VIDEO's left edge; omni keeps the app-wide 188px. */
#rback-fund .th-search-cell,
#rback-fund .tracker-body th:nth-child(5),
#rback-fund .tracker-body td:nth-child(5) { padding-left: 1.17%; padding-right: 0; }
/* !important for the same reason as the left pane's ST rule — the inline calc(33% - 204px) on the
   cells (index.html th :3181, td :7118) is the portal's tail and outranks an unflagged rule. Without
   it this sat on 76.2px rather than 7.15% (60.7px), putting ST ~8px past the omni's right edge. */
#rback-fund .tracker-body th:nth-child(6),
#rback-fund .tracker-body td:nth-child(6) { padding-left: 0 !important; padding-right: 7.15% !important; }

/* ── PORTAL LEDGER TABLES (FUNDED / PERSONAL / ENTITY) ─────────────────────────────────────────
   Same mechanism the two #rback- blocks above use: retune the shared <colgroup> from CSS by
   ancestor id instead of hand-editing six inline colgroups in the markup (three .tbl-head title
   strips + three .tracker-body tables, which must stay identical or the header badges drift off
   their columns).
   Scope is safe by construction. _mountTracker() MOVES #funded-flip-container and
   #intent-flip-container OUT of #view-dashboard and INTO the console panes, so a given table is
   under #view-dashboard or under #rback-*, never both. The two blocks can never fight, and neither
   needs to out-specify the other.
   !important is not optional here: <col style="width:..."> is an inline attribute and an unflagged
   author rule loses to it.
   TUNE THE SIX NUMBERS BELOW. They restate what the markup already has, so this block changes
   nothing until a number changes. All three portal tables — FUNDED, PERSONAL, ENTITY — share one
   colgroup shape, so each line moves that column in all three at once.
   These are the AUGUST proportions, restored 2026-08-28. The intervening version had col 4 as a
   hard 105px (the count badge's own width) with col 3 as calc(41% - 105px) absorbing it, plus an
   inline padding-right:calc(33% - 204px) on all six ST cells. Both are gone: the widths are back to
   plain percentages here, and ST is positioned by the .tracker-body th/td:nth-child(6) rule further
   down (6px each side) exactly as August had it.
   WHY THE calc HAD TO GO: percentage padding on a table cell resolves against the TABLE, so that one
   constant produced three different columns — âˆ'20px (clamped to 0) on August's 556px table, 17.8px
   at 672 with the pillar open, 190px at 1193 collapsed. A fixed 6px is width-independent, so ST now
   lands in the same place in every state. Do not reintroduce a percentage here.
   WATCH COL 4: the count/amount badge is a hard 105px, so col 4 must stay wider than that or the
   badge overflows its cell. 17% clears it down to a 618px table; below that it does not. */
#view-dashboard .tbl-head col:nth-child(1),
#view-dashboard .tracker-body col:nth-child(1) { width:  6% !important; }               /* # / dot   */
#view-dashboard .tbl-head col:nth-child(2),
#view-dashboard .tracker-body col:nth-child(2) { width: 20% !important; }               /* FIRST     */
#view-dashboard .tbl-head col:nth-child(3),
#view-dashboard .tracker-body col:nth-child(3) { width: 22% !important; }               /* LAST      */
#view-dashboard .tbl-head col:nth-child(4),
#view-dashboard .tracker-body col:nth-child(4) { width: 19% !important; }               /* AMOUNT / STATUS badge */
#view-dashboard .tbl-head col:nth-child(5),
#view-dashboard .tracker-body col:nth-child(5) { width: 16% !important; }               /* CONFIRMED / SUBMITTED */
#view-dashboard .tbl-head col:nth-child(6),
#view-dashboard .tracker-body col:nth-child(6) { width:  7% !important; }               /* ST        */

/* OMNI-SEARCH fills its cell. th-search-cell is colspan=2, so the box now measures exactly
   col 5 + col 6 — CONFIRMED/SUBMITTED plus ST — instead of asserting a 188px width that was
   chosen for the 1098px card and overflowed once the tracker went 2x2 at 541. */
#view-dashboard .tbl-head .th-search-cell .omni-wrap { display: flex; width: 100%; }
#view-dashboard .tbl-head input.th-search { width: 100% !important; }
/* 6px on the CELL, matching ST's own right padding (styles.css:841) so the search box ends on the
   same edge the ST column does instead of overhanging it. Not padding on the input — that would eat
   the box's interior; this is the gutter to the right of it. */
#view-dashboard .tbl-head .th-search-cell { padding-right: 8px; }
/* ST right padding 6 -> 4, header and every body cell. Scoped to #view-dashboard so the console
   (#rback-*) and stacked keep the 6px at styles.css:841. */
#view-dashboard .tracker-body th:nth-child(6),
#view-dashboard .tracker-body td:nth-child(6) { padding-right: 4px; }
/* The omni box carries padding-right:1.5rem (styles.css:929) to reserve room for the clear glyph.
   That was fine in a 188px box; in the 2x2 the box is col5+col6 and the 24px clips the last
   character of OMNI-SEARCH. Back to the 12px the shorthand at styles.css:926 sets on both sides. */
/* Right padding is short by one letter-space on purpose. letter-spacing:0.2em (styles.css:926) is
   applied after EVERY character including the last, so that trailing space lands inside the box and
   stacks on the padding — 8px left reads as 8, 8px right reads as 8 + 0.2em. calc keeps it correct
   if the font-size ever moves. */
#view-dashboard .tbl-head .omni-wrap > input.omni-search {
    padding-left: 8px !important;
    padding-right: calc(8px - 0.2em) !important;
}
/* $ badge sized to its worst case and no wider: "$" + "10,000,000.00" is 14 characters at 10px
   mono with 0.1em tracking (about 7px each) = 98, plus the badge px-2 = 114. Inline width on the
   element, so !important. Funded only — the only table with a th-amount cell. */
/* Both badges FILL their columns and the gaps are real padding, not leftover width. Before this the
   $-to-count gap was col 3's tail and the count-to-omni gap was col 4's tail, so the two were
   different numbers that drifted every time a column or a badge width moved — and the 105px count
   badge was overflowing col 4, exactly as the warning above predicts. At 100% neither can overflow
   at any table width, and both gaps are 9px because 9px is what they are set to.
   Same construction the console uses (#rback-fund .th-count). */
#view-dashboard .tbl-head .th-amount-badge { width: 100% !important; }
/* .th-count is NOT in that rule: both count badges carry an identical inline width:105px, and
   letting them fill col 4 instead made each one depend on its own table's column arithmetic. They
   have to be the same size as each other, so they keep the fixed width. Col 4 must therefore stay
   above 105px — see the WATCH COL 4 warning above. */
#view-dashboard .tbl-head .th-amount { padding-right: 9px; }
#view-dashboard .tbl-head .th-count-cell { padding-right: 9px; }
#view-dashboard .tbl-head .th-search-cell { padding-left: 0; }
#view-dashboard #intent-chart-view > .px-4 { padding-right: 8px; }
#view-dashboard #funded-flip-container .back > .px-4 { padding-right: 8px; }
#view-dashboard #intent-chart-hover-ent { margin-right: 0; }
#view-dashboard #intent-target-badge,
#view-dashboard #intent-chart-hover-per,
#view-dashboard #funded-target-badge { margin-right: 9px; }
#view-dashboard #funded-chart-hover { width: 100% !important; margin-right: 0; }

/* No omni gets two edges — console included. focus:ring-1 paints a SECOND 1px ring outside the
   border and its compiled default is --tw-ring-color:var(--foundation-blue), which is the double
   edge. On the CLASS so it reaches the console's eight inputs as well as the tracker's three, and
   so it survives _mountTracker() moving the tracker cards into the console.
   Colour needs no help: every omni already declares its own focus:border-[...] — blue on the console,
   whose cards are foundation-blue anyway. The three rules below only pin the tracker's. */
input.omni-search:focus { box-shadow: none !important; }

/* OMNI FOCUS — one border, in the card's own colour. */
/* NOT prefixed with #view-dashboard: _mountTracker() moves these containers into the console, so a
   rule rooted at the view stops matching exactly when the card is on the console side — which is why
   they went blue there. The container ids travel WITH the cards, so these follow them. */
#funded-flip-container input.th-search:focus { border-color: var(--status-green)  !important; }
#intent-flip-container .front input.th-search:focus { border-color: var(--status-amber)  !important; }
#intent-flip-container .back  input.th-search:focus { border-color: var(--status-purple) !important; }
/* The body columns under those badges, on the same 9px. Beats both the 0/0 at styles.css:881 and
   funded's 4px at :885 on specificity — (1,2,1) against (1,1,1). Without this the head sits on 9px
   and the cells beneath it on 0 and 4, so the badge edges and the data edges are three different
   vertical lines. */
#view-dashboard .tracker-body th:nth-child(3),
#view-dashboard .tracker-body td:nth-child(3),
#view-dashboard .tracker-body th:nth-child(4),
#view-dashboard .tracker-body td:nth-child(4) { padding-right: 9px; }

/* Member profile head tracks the BODY grid below it, not .tracker-head's 50/50. 200px + gap-6 (24px)
   are the same numbers the body uses, so the member number begins on exactly the x where ACTIVE
   HOLDINGS and the rest of the labels begin, and the join date ends on the card's right inset. */
@media (min-width:1024px) {
    #mod-member-profile .tracker-head { grid-template-columns: 200px minmax(0,1fr); gap: 24px; }
}


/* ── CHART HEADERS ─────────────────────────────────────────────────────────────────────────────
   The markup is the ORIGINAL flex row — group div, hover badge, chevron — unchanged. Below 1024 that
   is exactly what renders, which matters because the chevron is the only way to swap between the two
   charts on a phone (chartpage-funded / chartpage-intent hide one each).
   Desktop turns the same markup into the 4-column grid by promoting the wrapper divs with
   display:contents, so their children become grid items. The chevron is lg:hidden, so it is not an
   item at all up here and the counts work out to exactly four.
   px-4 is dropped at this breakpoint so the percentages resolve against the whole card, the way the
   table colgroup below them does; column 1's inset moves onto its first child instead.
   Placement is by id, never by position — the chevron is the last DOM child but renders nothing. */
@media (min-width:1024px) {
  /* gap:0 is load-bearing — .tracker-head's desktop rule sets gap:12px, and these four track
     percentages were written to sum to 100% of the content box with no gap. Inheriting it added
     3 x 12 = 36px, so the row measured 708 against a 672 box and clipped the right-hand badge. */
  #funded-chart-head, #intent-chart-head { display:grid; align-items:center; gap:0; padding-left:0; padding-right:0; }
  #funded-chart-head > div, #intent-chart-head > div { display:contents; }
  #funded-chart-head { grid-template-columns: 6% 31.75% 21.42% 40.83%; }
  #intent-chart-head { grid-template-columns: 45.95% 17.6%  17.6% 18.85%; }
  #funded-chart-head .chart-head-grp > :first-child,
  #intent-chart-head .chart-head-grp > :first-child { margin-left:16px; }
  /* Both chart titles start on the same x. FUNDED's title sits in column 2 because column 1 (6%) is
     its status dot; PIPELINE has no dot, so its title was the column-1 item and began ~49px further
     left. Give intent a matching empty 6% column and push its title into column 2.
     The 6 comes out of the title's own track (45.95 -> 39.95), so no other column moves and the row
     still sums to 100. margin-left:0 because the rule above puts 16px on the first child — correct
     for FUNDED, where that child is the dot in column 1, but here it would stack on top of the 6%
     and land the title 16px right of FUNDED's. */
  /* 5 tracks, and column 1 is the STATUS DOT — the same shape #funded-chart-head has always had.
     Before the dot existed this column was empty and the title was pushed into column 2 with
     grid-column-start; that override is gone now, because with a real first child the title lands in
     column 2 on its own and picks up the shared margin-left:16px rule like funded's dot does.
     The 6 came out of intent's own title track (45.95 -> 39.95), so no other column moved. */
  #intent-chart-head { grid-template-columns: 6% 31.43% 21.42% 21.42% 19.73%; }
  /* PORTAL: nothing overhead to anchor to (the KPI cards are in the other column), so built from the
     right edge — last badge on a 16px inset mirroring column 1, 8px between. */
  #funded-target-badge,
  #intent-target-badge,
  #intent-chart-hover-per { justify-self:end; margin-right:8px; }
  #funded-chart-hover,
  #intent-chart-hover-ent { justify-self:end; margin-right:16px; }
  #funded-target-badge, #intent-target-badge { width:105px; height:32px; }

  /* CONSOLE: same columns, but every badge is read off the metrics grid above instead.
     TARGET is the SCROLL select, $ is the VIDEO select — gap included, by construction. */
  #rback-fund #funded-chart-head { grid-template-columns: 6% 50.5% 12.37% 31.13%; }
  /* 5 tracks, matching the portal: a 6% leading column so PIPELINE's title starts on the same x
     within its pane as CAPITAL VELOCITY does in its own — #rback-fund's column 1 is 6% (the status
     dot) and its title sits in column 2, while intent has no dot and was starting at column 1.
     The 6 comes out of intent's own title track (52.3 -> 46.3), so no other column moves and the row
     still sums to 100. The portal's `grid-column-start:2; margin-left:0` on the title then applies
     here too, which is why there is no override: both surfaces now want the same placement. */
  #rback-int  #intent-chart-head { grid-template-columns: 6% 46.3% 13.65% 13.59% 20.46%; }
  #rback-fund #funded-target-badge { width:90.76%; height:32px; justify-self:end; margin-right:0; }
  #rback-fund #funded-chart-hover  { width:91.72%; justify-self:start; margin-left:3.743%; margin-right:0; }
  #rback-int  #intent-target-badge    { width:92.67%; height:32px; justify-self:start; margin-right:0; }
  #rback-int  #intent-chart-hover-per { width:93.07%; justify-self:start; margin-right:0; }
  #rback-int  #intent-chart-hover-ent { width:61.83%; justify-self:start; margin-right:0; }
  /* Chart body: px-4 gives 16px, but the header's right inset is 12 — the metrics grid's own
     trailing gap, which the $ badge inherits from VIDEO. Without this the plot stops 4px short. */
  #rback-fund #funded-chart-head + div { padding-right:12px; }
}
/* MOBILE: those widths are desktop measurements — 105 for the hover badges, 150 for the $ — and the
   header only has ~326px on a phone once the dot, title and chevron are in. Content-sized instead,
   which is narrow enough for all of them to coexist on hover. !important because the widths are
   inline style attributes.
   TARGET and CAP go to 32px so every badge in the row is one height; the markup's h-[20px] is a
   class, so the id outranks it without !important. The markup note about 20px aligning to the title
   still applies where nothing overrides it. */
@media (max-width:1023px) {
  #funded-chart-hover,
  #intent-chart-hover-per,
  #intent-chart-hover-ent { width: auto !important; }
  #funded-target-badge, #intent-target-badge { height: 32px; }
}
#roster-flip.flipped > #rface-front { display: none; }
#roster-flip.flipped > #rface-back { display: flex; }
#rback-int > .flip-container, #rback-fund > .flip-container { height: 100% !important; }
 
.blueprint-card {
    background: var(--card-graphite);
    border: 1px solid var(--border-silver);
    position: relative;
    z-index: 2;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.05);
    border-radius: 2px !important;
}

/* Universal Institutional Haptics */
.tactile-btn {
    transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
    user-select: none;
}
.tactile-btn:active:not(:disabled) {
    transform: translateY(2px);
}
/* Red-X delete button: draw the × from two centered bars so it's dead-center regardless of font metrics
   (the text glyph sits low). currentColor = the button's own color. */
.dj-x-btn { position:relative; }
.dj-x-btn::before, .dj-x-btn::after { content:''; position:absolute; top:50%; left:50%; width:11px; height:2px; background:currentColor; border-radius:1px; }
.dj-x-btn::before { transform:translate(-50%,-50%) rotate(45deg); }
.dj-x-btn::after  { transform:translate(-50%,-50%) rotate(-45deg); }
.btn-active-pressed {
    box-shadow: inset 0 3px 6px rgba(0,0,0,0.2) !important;
}
 
/* Navigation */
.nav-container {
    background-color: var(--card-graphite);
    padding: 4px;
    border: 1px solid var(--border-silver);
    border-radius: 2px;
    display: flex;
    align-items: center;
    width: 100%;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Only while page dividers hang off the bottom edge: they have straight sides, and a 2px curve
   there leaves a sliver at the join that no vertical overlap can close. Toggled by renderNavPages. */
/* 1px under the sticky table-header bar on the engine tables (cap table, members, operations).
   The bar sits flush against the scrolling body, so the body's viewport started exactly where the
   header ended and the row at the bottom edge showed a 1px sliver. Two px of padding here shorten
   the viewport by the same amount and the partial row falls out of view. MEASURED, not guessed: at 0 the body viewport was 677 = 13.019 rows of 52, so 1px of row 14 showed; 2px gave 675 = 12.981 and clipped row 13. 1px lands the viewport on 676 = exactly 13 rows. If the row height or the card ever changes, re-measure rather than nudging.
   No effect on the card: it is h-[760px] flex flex-col overflow-hidden with the body flex-1, so the
   body absorbs the pixel and the 828 / 844 / 1134 column alignment is untouched.
   Named rather than a pb-px utility so the five bars stay in step and it survives a rebuild. */
.th-bar { padding-bottom: 1px; }
/* Tried per-table header heights here (th height 56 / 60) so the label bar absorbed each card's
   leftover instead of it sitting under the last row. Reverted 2026-08-31: a taller label bar reads
   as less refined than the padding it replaced. Do not re-attempt without a look at it first. */

/* ── MEMBERS · DEEP DIVE ───────────────────────────────────────────────────────────────────────
   DEEP DIVE flips the long card and the history card, and re-lays the column around them. Only
   three declarations are needed because the grid does the rest: the left stack (profile 407 + 16 +
   submit 361) and the long card are both 784 today, so hiding the submit engine and shrinking the
   long card drops the grid row to 407, and the history card below rises on its own. No positioning,
   no z-index — the collision that would have needed it disappears once the submit engine goes.

       FRONT                          DIVED
       profile          152 → 559     unchanged
       submit engine    575 → 936     hidden
       tier2 (long)     152 → 936     152 → 559    784 → 407, level with the profile card
       history          952 → 1220    575 → 1220   268 → 645, keeps its bottom, gains 377 for the chart

   407 + 16 + 645 = 1068, so the column still totals the same in both states and both dived cards
   land on the 559/575 seam the profile card already sets.

   1024 guard: mobile has no two-column pillar to re-lay, and its cards are h-auto. Unlayered so it
   beats the h-[784px] / h-[268px] utilities without !important.

   MEASURED after the first build (dived, >=1900): tier2 landed 152 -> 559 exactly level with the
   profile card, but history landed at 591 instead of 575 and ran to 1236, overflowing #view-members
   by 16 (scrollHeight 1084 vs 1068). Cause: space-y-4 is
   `> :not(:last-child) { margin-block-end: 16px }`, and display:none does NOT remove an element from
   :not(:last-child) — the profile card is still not the last child of the left pillar, so it kept a
   16px margin against a hidden sibling and made the grid row 423. Zeroing it below is what makes the
   645 land: 407 + 16 + 645 = 1068. */
@media (min-width:1024px) {
    #view-members.deep-dive #mod-member-form      { display: none; }
    #view-members.deep-dive #mod-member-left > *  { margin-block-end: 0; }
    #view-members.deep-dive #mod-member-tier2     { height: 407px; }
    #view-members.deep-dive #mod-member-history   { height: 645px; }
}

/* Gap below the nav shell — now #nav-band's padding-bottom (pb-1.5 lg:pb-4), the SAME 6 / 16 this
   rule used to set as a margin. It had to stop being a margin: the nav sits inside #app-header now,
   and a bottom margin on the last child escapes the opaque box (margins collapse through a parent
   with no bottom padding or border), which would leave a see-through strip between the pinned header
   and the page — exactly the failure the mobile marquee comment in the markup warns about. As
   padding it is inside the header, so the breathing room is opaque and scrolls nothing through it.
   Desktop keeps the 16 module (app-main py-4/gap-4, .terminal-grid gap-4, left column space-y-4);
   mobile keeps its 6 because the nav sits tighter to the content there. */

/* Desktop nav row joins the 52px module. Until now this row had NO height and simply took whatever
   the buttons came to — 35.2px of button (12px line box + .nav-btn's 0.6rem padding) plus the
   container's 4px padding and 1px borders = 45.2px, i.e. a number nobody chose.
   Scoped to desktop, and that scoping is REQUIRED, not stylistic: below 1024 #nav-tabs is
   `grid grid-cols-3`, so the buttons sit on two rows (~82px) and an unscoped 52 would clip the
   second row. This is the reverse of the card title rows, where 52 already applied at both widths
   and scoping it to lg would have been the thing that broke mobile.
   Buttons are untouched — .nav-container is already align-items:center, so they centre in the 50px
   content box with 7.4px above and below. */
@media (min-width: 1024px) {
    .nav-container { height: 52px; box-sizing: border-box; }
}

.nav-container.has-dividers { border-bottom-left-radius:0; border-bottom-right-radius:0; }
.nav-container:hover {
    border-color: var(--foundation-blue) !important;
    box-shadow: 0 10px 20px -5px color-mix(in srgb, var(--foundation-blue) 15%, transparent) !important;
}
/* Mobile: nav border always visible, not just on hover */
@media (max-width: 1023px) {
    .nav-container {
        border-color: var(--foundation-blue) !important;
    }
    .nav-btn:not(.nav-tab-active):not(.btn-apply) {
        background-color: var(--terminal-recess);
    }
    .dark-mode .nav-btn:not(.nav-tab-active):not(.btn-apply) {
        background-color: #2d3748;
        color: #ffffff;
    }
}

@keyframes heartbeat {
    0%   { transform: scale(1);    opacity: 1; }
    14%  { transform: scale(1.06); opacity: 1; }
    28%  { transform: scale(1);    opacity: 1; }
    42%  { transform: scale(1.04); opacity: 0.9; }
    56%  { transform: scale(1);    opacity: 1; }
    100% { transform: scale(1);    opacity: 1; }
}
.btn-apply {
    animation: heartbeat 1.8s ease-in-out infinite;
    background-color: var(--status-green) !important;
    color: #ffffff !important;
}
.dark-mode .btn-apply {
    color: #0a0c10 !important;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.nav-btn {
    font-size: 12px;
    font-weight: 900;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-muted);
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    /* 0.6rem = 9.6px, the engine's value. It was 8px here, and that 1.6px per side is what made the
       merged nav bar sit shorter than the engine's — the bar height is this number, not the buttons.
       The !important on both declarations existed only to outrank Tailwind utilities under v3; on v4
       unlayered CSS wins on its own, so they are gone. */
    padding: 0.6rem 0.8rem;
    border: none;
    background: transparent;
    border-radius: 2px;
    cursor: pointer;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
}
.nav-btn:hover:not(.nav-tab-active) { background-color: var(--nav-btn-hover); color: var(--foundation-blue); }
/* Nav tab size lives here, not in the markup: renderNavTabs emitted `!text-[10px] lg:!text-xs` and
   the lg: half never compiled, so desktop rendered at 10px against MasterBuild's 12. .nav-btn's own
   12px now stands on desktop with the inherited 18px line box; mobile keeps 10. */
@media (max-width: 1023px) {
    #nav-tabs .nav-btn { font-size: 10px; }
}

.nav-tab-active { background-color: var(--nav-active-base); color: #ffffff !important; box-shadow: inset 0 0 0 1px var(--foundation-blue-hover), 0 1px 4px rgba(0,0,0,0.3); }
/* RING mode: the selected tab reads as an outline instead of a filled block. The inset ring already
   exists in .nav-tab-active — it is simply no longer covered by the fill. Text moves to the accent
   colour because white on a transparent tab is invisible on a light surface (that exact pairing is
   what the .nav-fx-off bug produced). --tab-color wins when the console gave the tab one. */
body.nav-ring .nav-btn.nav-tab-active {
    background-color: transparent !important;
    background-image: none !important;
    color: var(--tab-color, var(--foundation-blue)) !important;
    box-shadow: inset 0 0 0 1px var(--tab-color, var(--foundation-blue));
}
body.nav-ring .nav-btn.nav-tab-active.btn-apply { color: var(--status-green) !important;
    box-shadow: inset 0 0 0 1px var(--status-green); }
 
/* ── CONSOLE BUTTON SYSTEM — TWO TIERS ──────────────────────────────────
   Tier 1: Primary commands (header bar, card actions)
   h-[34px] px-5 text-[11px] tracking-widest — EXPORT PDF, CLOUD SYNC, etc.
   Tier 2: Row actions (table buttons)
   h-[28px] px-3 text-[10px] tracking-widest — MARK FUNDED, EDIT, REMOVE, etc.
   ──────────────────────────────────────────────────────────────────────── */
.btn-action-uniform {
    box-sizing: border-box;
    height: 32px;
    padding: 0 20px;
    font-weight: 900;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    transition: all 0.15s;
    cursor: pointer;
    text-align: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    border-radius: 2px;
    min-width: unset;
}
.btn-row {
    box-sizing: border-box;
    height: 32px;
    padding: 0 10px;
    font-weight: 900;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    transition: all 0.15s;
    cursor: pointer;
    text-align: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    border: 1px solid;
    border-radius: 2px;
    min-width: unset;
}
.btn-standard { background-color: var(--foundation-blue); color: white; border: none; }
.btn-standard:hover { filter: brightness(1.1); }
/* Chip role: small utility controls — language, theme, LOG OFF, country picker, chart toggles,
   PERSONAL/ENTITY. Named so these stop improvising their padding inline. Horizontal only, so
   each button keeps its own py-* and no height changes. */
.btn-chip { padding-left:8px; padding-right:8px; }
/* MODE + LOG OFF carry the extra width the launch row needs: that row is width-locked to
   #nav-controls, and the real-time string overruns its value cell at the base 8px chip padding.
   EN|ES is excluded on purpose — alignLaunchRow already pins the langbox to the FUND LAUNCH label
   width, so widening those would only grow the label cell and take the space straight back.
   Desktop only; the mobile header has no room to give. */
@media (min-width:1024px) {
    #nav-controls > button.btn-chip { padding-left:13px; padding-right:13px; }
}
/* Mobile: LOG OFF is icon-only (~32px), but the launch row's last cell mirrors it and must hold the
   date + time + chevron. Widening the button is the only lever that widens that cell. */
@media (max-width:1023px) {
    #nav-controls > button.btn-exit { padding-left:24px; padding-right:24px; }
}

 
/* Text Labels */
.text-label {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-muted);
    display: flex;
    align-items: center;
}

@keyframes breathe { 0%, 100% { opacity: 1; box-shadow: 0 0 8px currentColor; } 50% { opacity: 0.4; box-shadow: 0 0 0px transparent; } }
/* background-color:currentColor — the dot had NO FILL. MasterBuild's is 14px with a constant
   `box-shadow: 0 0 10px currentColor`, so its glow alone reads as a dot; the portal halved it to 7px
   and swapped the constant glow for `breathe`, which goes `0 0 0px transparent` at 50%. The result
   was a 7px halo that vanishes twice a second — #status-dot's colour WAS changing on the freeze
   toggle (the JS is byte-identical to MasterBuild's), there was just nothing solid to show it on.
   Safe globally: all seven other .status-dot-pulse elements pair bg-[var(--x)] with a matching
   text-[var(--x)] on the same element or its h3, so currentColor resolves to the colour they already
   paint. Verified 2026-08-31 across index.html:2213, 2246, 2281, 2312, 2343, 3222, 3334. */
.status-dot-pulse { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background-color: currentColor; animation: breathe 2.5s infinite ease-in-out; }
/* Also never imported (engine/styles.css:435). The dot is a click target — it drives the god-mode
   zoom — and without this it gave no press feedback at all. */
.status-dot-pulse:active { transform: scale(0.9) !important; transition: transform 0.05s ease-out !important; }

/* ── THE REST OF WHAT THE IMPORT DROPPED ───────────────────────────────────────────────────────
   Found 2026-08-31 by diffing engine/styles.css against this file: 30 selectors absent, most of them
   deliberately (the engine's own #app-header, the .dark-mode input rules that restate a value
   --terminal-recess already flips, scrollbar thumbs the portal hides outright). These three are not.

   1. FLIP-CARD HOVER (engine :81, :84, :101). .flip-indicator exists here at :1581 with opacity 0.15
      and a transition already declared for opacity AND transform — it was built to light up and
      nothing ever lit it. This is almost certainly the "hover ring" that sat unresolved on the
      issues list: the element was present, so the bug looked like a mystery rather than a gap.
   2. .finesse-table th:active — every sortable header, no press feedback. The portal has no
      equivalent at all.
   3. .btn-action-uniform:active — 12 of 52 of these buttons do not also carry .tactile-btn, so
      .tactile-btn:active never reached them. Restoring the engine's selector covers all 52; the 40
      that do carry tactile-btn get the same transform from both, which is a no-op. */
.flip-container .blueprint-card {
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.flip-container:hover .blueprint-card {
    border-color: var(--foundation-blue) !important;
    box-shadow: 0 10px 20px -5px rgba(0, 160, 220, 0.15) !important;
}
.flip-container:hover .flip-indicator {
    opacity: 0.8;
    transform: translate(2px, 2px);
}
.btn-action-uniform:active,
.finesse-table th:active,
#sys-mode-btn:active,
#nav-fund-title:active {
    transform: translateY(1px) !important;
    transition: transform 0.05s ease-out !important;
}
/* #sys-mode-btn does not carry .tactile-btn, so nothing else reaches it. #nav-fund-title only
   became a click target on 2026-08-31 (single tap reloads, double tap holds the stacked view) — the
   engine's h1:active covered it there and the portal had no equivalent. The engine's remaining
   :active selectors are genuinely dead here: #theme-toggle-btn was deleted on import, .chart-time-btn
   carries .tf-pill in all 8 places, .nav-btn carries .tactile-btn, and clearReconInputs' button is
   now .btn-action-uniform — all already covered. */
 
/* Tables */
.custom-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }
.custom-scrollbar::-webkit-scrollbar { width: 0; height: 0; }
 
.finesse-table { border-collapse: separate; border-spacing: 0; table-layout: fixed; width: 100%; }
/* Unified Sticky Header Update */
.finesse-table th { position: sticky; top: 0; z-index: 10; font-size: 11px; font-weight: 900; letter-spacing: 0.1em; color: var(--text-muted); text-transform: uppercase; padding: 6px 16px; background: var(--table-th-bg); border-bottom: 1px solid var(--border-silver); box-shadow: 0 1px 0 var(--border-silver); cursor: pointer; transition: color 0.1s; }
.finesse-table th:hover { color: var(--foundation-blue); }
.finesse-table td { font-size: 12px; padding: 6px 16px; border-bottom: 1px solid var(--table-td-border); color: var(--text-main); height: 52px; }
/* Roster only: ST / FIRST / LAST tighten to 6px so they align under the clock box above them.
   Every other column, and every other table, keeps the 16px default. */
#roster-card-card th:nth-child(2), #roster-card-card td:nth-child(2),
#roster-card-card th:nth-child(3), #roster-card-card td:nth-child(3),
#roster-card-card th:nth-child(4), #roster-card-card td:nth-child(4) { padding-left: 6px; }
#roster-card-card th:nth-child(9), #roster-card-card td:nth-child(9) { padding-left: 0; }   /* DATE: reclaim 10px so the header checkbox + label stay on one line */
/* Verify Intent header: 24px right padding so CANCEL's edge lines up with the values below it, which
   sit in a px-6 body. .tracker-head's own 16px left everything in the header 8px out of column.
   Written here rather than as a `pr-6` utility on the element: .tracker-head is UNLAYERED and beats
   anything in @layer utilities at any specificity, so the utility would have compiled, applied
   nothing, and looked like a measurement error. Left padding stays 16 — the title reads as a header,
   not as another row of the table below it. */
#submit-verification-overlay .tracker-head { padding-right: 24px; }

/* APM Split Audit Trail only (back of #mod-chart-nav). Seven columns in a 661px card, and the 6px
   16px default spends 224px of it — a third of the table — before a glyph renders. The header text
   totals 361.9px measured, so 585.9 of 661 is genuinely needed and there is nothing left to give the
   columns that wrapped (POST-UNITS was 10px short). 10px sides frees 84px, which is what lets the
   colgroup at index.html:2465 clear every header at BOTH the 100% and the two-step-zoom-out
   measurement instead of only the first. Widths live in that colgroup — change the two together. */
#mod-chart-nav .back .finesse-table th,
#mod-chart-nav .back .finesse-table td { padding-left: 10px; padding-right: 10px; }
/* Activity cards: REVIEWED / ACTION sit flush under the omni box, same as the roster DATE. */
#access-requests-card th:nth-child(6), #access-requests-card td:nth-child(6),
#portal-activity-card th:nth-child(6), #portal-activity-card td:nth-child(6) { padding-left: 0; }
/* SUBMITTED / TIMESTAMP: no right padding, so the full datetime has the column to itself. */
#access-requests-card th:nth-child(4), #access-requests-card td:nth-child(4),
#portal-activity-card th:nth-child(4), #portal-activity-card td:nth-child(4) { padding-left: 6px; padding-right: 0; }
/* Portal tracker tables (#/ST/SUBMITTED) — same trick the roster uses above. The default 6px 16px
   spends 32px of every cell on padding, which on a 642px table is 192px across six columns. The
   narrow ones don't need it: # holds two digits, ST holds two letters. Widths are untouched, so the
   .tbl-head colgroup still lines the count/search cluster up with STATUS/SUBMITTED (§16aa).
   SUBMITTED drops its right padding only, matching the console's timestamp columns. */
.tracker-body th:nth-child(6), .tracker-body td:nth-child(6) { padding-left: 6px; padding-right: 6px; }   /* ST */
/* DESKTOP ONLY. Both of these are 642px-card tuning and neither existed before 2026-08-25; on mobile
   the columns fall back to .finesse-table's 6px 16px, which is what puts the body # on the same 16px
   as the header row's own padding. Ungated, the 15px left col 1 (21.4px wide on a phone) with 0.4px
   of content and knocked the # a pixel off the title above it.
     #  — 15px matches the gap ST leaves on the right: ST is centred in a 45px column, so its 14.4px
          of text floats 15.3px clear of the table's right edge, and the two ends then match.
     SUBMITTED — 9px puts it on the same edge as the omni-search above it, which moved to 9px so the
          three header badges space evenly. No right padding; the column has slack to spare. */
@media (min-width:1024px) {
  .tracker-body th:nth-child(1), .tracker-body td:nth-child(1) { padding-left: 15px; padding-right: 6px; }
  .tracker-body th:nth-child(5), .tracker-body td:nth-child(5) { padding-left: 9px; padding-right: 0; }
}
/* STATUS (col 4): the cell holds a fixed-width badge, so its 32px of padding buys nothing and cost
   the badge 32 of the column. Desktop-only — mobile right-anchors the same badge to a
   16px inset using this exact padding (see the max-width:1023px block), so clearing it there would
   unpin it from the count badge above. */
@media (min-width:1024px) {
    .tracker-body th:nth-child(4), .tracker-body td:nth-child(4) { padding-left: 0; padding-right: 0; }
    /* FUNDED body only: AMOUNT is right-aligned text, not a badge, so with zero padding it runs to
       the column edge while the count badge above stops 105px in. 4px brings the .00 onto the
       badge's right edge. (109px column - 105px badge = 4.) */
    #funded-table-body td:nth-child(4) { padding-right: 4px; }
}
/* Sort affordance, derived rather than typed. It used to be a literal ↕ pasted into the roster,
   activity and access headers, so every other sortable header — including three in the portal's own
   tracker — sorted with nothing to say so. Keying off the handler makes the reverse true as well:
   no arrow now means genuinely not sortable.
   No leading space in the content: the glyph it replaced was typed flush against the label (VER↕),
   and a space is enough to wrap the two narrowest headers, ST and VER.
   Two selectors, not one: console's own sort function is named srt(), but cap table/Unitization
   Engine/Verifier Hub/History and Audit Hub call sortCapTable()/sortQueueTable()/
   sortAdminQueueTable()/sortHistoryTable() — none contain the substring "srt", so they were silently
   excluded from this rule the whole time it existed. 2026-09-03: every onclick containing "sort"
   anywhere in the file is one of those four genuine sort handlers, so the broader match is safe. */
@media (min-width:1024px) {
  .finesse-table th[onclick*="srt"]::after,
  .finesse-table th[onclick*="sort"]::after { content: '\2195'; opacity: 0.45; }
}
.finesse-table tr:hover { background: var(--table-tr-hover); }
/* STATUS column centred, so the pill lines up with the count badge above it. */
#access-requests-card th:nth-child(5), #access-requests-card td:nth-child(5),
#portal-activity-card th:nth-child(5), #portal-activity-card td:nth-child(5) { text-align: center; padding-right: 0; }
.finesse-table tbody tr:nth-child(even) { background: var(--table-stripe); }
/* Lifted column-header: th must NOT be individually sticky — the wrapper does the sticking */
.finesse-table.lifted-head th { position: static; top: auto; }
/* Console-controlled tracker column visibility — driven by config/settings.trackerColumns via
   applyTrackerColumns() (admin ticks a roster-header checkbox). check = show; an "off" field drops its
   column as a WHOLE UNIT (col + th + td together) so the remaining columns stay aligned with their
   headers — nothing shifts inside a cell. Classes live on <html>. Amount (funded col 5) is always shown.
     name      -> First(2) + Last(3)  on individual + entity + funded
     stloc     -> col 6  on all three (moved to LAST so the omni-search can span cols 5+6)
     status    -> col 4  on individual + entity
     submitted -> col 5  on individual + entity
     confirmed -> col 5  on funded */
html.tkoff-name #intent-table-body :is(col,th,td):nth-child(2),
html.tkoff-name #intent-table-body :is(col,th,td):nth-child(3),
html.tkoff-name #sec-table-body :is(col,th,td):nth-child(2),
html.tkoff-name #sec-table-body :is(col,th,td):nth-child(3),
html.tkoff-name #funded-table-body :is(col,th,td):nth-child(2),
html.tkoff-name #funded-table-body :is(col,th,td):nth-child(3),
html.tkoff-submitted #intent-table-body :is(col,th,td):nth-child(5),
html.tkoff-submitted #sec-table-body :is(col,th,td):nth-child(5),
html.tkoff-status #intent-table-body :is(col,th,td):nth-child(4),
html.tkoff-status #sec-table-body :is(col,th,td):nth-child(4),
html.tkoff-confirmed #funded-table-body :is(col,th,td):nth-child(5),
html.tkoff-stloc #funded-table-body :is(col,th,td):nth-child(6),
html.tkoff-stloc #intent-table-body :is(col,th,td):nth-child(6),
html.tkoff-stloc #sec-table-body :is(col,th,td):nth-child(6) { display:none !important; }
/* Admin console sweet-spot width cap — tweak --admin-maxw to taste */
:root { --admin-maxw: 1728px; }
.admin-maxw { max-width: var(--admin-maxw); margin-left: auto; margin-right: auto; width: 100%; }

.scroll-progress-track { position:absolute; bottom:0; left:0; right:0; height: 2px; width: 100%; background: var(--border-silver); overflow: hidden; flex-shrink: 0; }
.scroll-progress-fill { height: 100%; width: 0%; background: var(--foundation-blue); transition: width 0.05s linear; }

/* Omni-Search: Terminal-recessed black slots */
input.omni-search {
    font-family: 'JetBrains Mono', monospace;
    padding: 6px 12px !important;
    height: 32px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    font-size: 12px;
}
/* Clear-X shown inside any omni-search once it has text. Wrapper positions the button; the input
   gets extra right padding so typed text never runs under the ×. */
.omni-wrap { position: relative; display: inline-flex; flex: 0 0 auto; align-items: center; }
.omni-wrap > input.omni-search { padding-right: 1.5rem !important; }
.omni-clear { position: absolute; top: 0; bottom: 0; right: 6px; margin: auto; width: 16px; height: 16px; display: none; align-items: center; justify-content: center; padding: 0; line-height: 1; font-size: 16px; font-weight: 900; color: var(--text-muted); background: transparent; border: 0; cursor: pointer; }
.omni-clear:hover { color: var(--foundation-blue); }
/* EVERY input inside the gate stays at 16px so iOS Safari never zoom-jumps on focus (it zooms any
   input under 16px). This covers name/email/phone AND the SMS code field + the country search — and
   any future gate input. The viewport lock that would allow smaller text broke the VERIFY tap, so
   16px is the floor. !important: the 16px is functional (prevents the zoom), not cosmetic. */
#gate input { font-size: 16px !important; height: 44px !important; }
/* Uniform 44px control height across the whole login flow (inputs, both submit buttons, slider).
   Buttons need the override because btn-action-uniform is a shared 32px used elsewhere. */
#gate-btn, #gate-verify-btn { height: 44px !important; }
/* The 4 identity fields also get a calmer weight/spacing + tighter line so the 16px boxes aren't tall. */
#gate-first, #gate-last, #gate-email, #gate-phone {
    font-weight: 500;
    letter-spacing: 0.05em;
    line-height: 1.2;
}

/* Tracker table headers — desktop is FLEX (title absorbs slack via margin-right:auto;
   the count/search cluster sizes to its own fixed width on every table, so all three
   are consistent by construction and the inline grid-template-columns are inert here).
   Mobile (<1024px) collapses to a two-column grid: title/count on the left, omni-search/$ on the right. */
/* Tracker header = real <table> sharing the body colgroup, so count(col5)/search(col6) lock to
   STATUS/SUBMITTED at any width; each cell mirrors its column's align+inset. Bible §16aa. */
.tbl-head { table-layout:fixed; width:100%; border-collapse:separate; border-spacing:0; }
.tbl-head td { height:52px; vertical-align:middle; padding:0; }
/* The status dot gets col 1 to itself, centred over the # column below. That is what frees the
   title: FUNDED POSITIONS needs ~150px and had 164 shared with the dot, so it wrapped. */
/* Dot on the same 15px inset the # uses in the body below, so the two sit on one vertical line. */
.tbl-head .th-dot-cell { text-align:left; padding-right:0; }
/* Desktop only, for the same reason as col 1 above. On mobile .tbl-head tr supplies the 16px and
   the dot cell must add nothing — before today the dot sat inside the title div and inherited it. */
/* 15, NOT 16 — do not "correct" this to match .finesse-table's 16px cell padding. The dot is not
   aligned to the # column; it reads against the FIRST NAME column and the title beside it. Changed
   to 16 on that wrong assumption and it visibly threw the whole head out. Committed value at
   c43f03c. */
@media (min-width:1024px) { .tbl-head .th-dot-cell { padding-left:15px; } }
.tbl-head .th-title { padding-left:1rem; }
.tbl-head .th-count-cell { text-align:left; padding-left:1rem; }   /* funded overrides to right (money) inline */
.tbl-head .th-amount { padding-left:1rem; }
.tbl-head .th-search-cell { text-align:left; padding-left:1rem; }
/* The header row carries the same two fixed-width controls the body columns carry, so it has to
   follow them. Desktop-only, because the mobile block below re-lays this row as a 2-col grid where
   the same padding is what pins the count badge to the 16px inset.
   The count badge sits on col 4 over STATUS, whose cell also has pad 0, so the two stay stacked.
   The search keeps its 16px so it lines up with the SUBMITTED date under it, and its fixed 188px
   fits again now that it spans cols 5+6 (212px). */
@media (min-width:1024px) {
    /* Count badge is a fixed 105px in a 109px col 4 — the padding is the whole margin, so it goes.
       th-amount keeps its 16px: that is what puts the money badge on the same left edge as the
       LAST column under it, which shares col 3. */
    .tbl-head .th-count-cell { padding-left:0; padding-right:0; }
    /* 9px, not 16: it makes the count->search gap match the money->count gap (13px each), so the
       three header controls read as evenly spaced. The count badge cannot move — it is 105px in a
       109px column with its padding already at zero — so the search is the only one with give. */
    .tbl-head .th-search-cell { padding-left:9px; }
}
/* Mobile: drop app-main's own top padding so the header's inner 16px is the only gap above the menu. */
@media (max-width:1023px){ #app-main { padding-top:0 !important; } }
@media (max-width:1023px){
    /* Header: title + omni-search share row 1; count (+ funded $) drop right-aligned to row 2. */
    .tbl-head, .tbl-head tbody { display:block; }
    .tbl-head colgroup { display:none; }
    /* Row 1: title (left) + funded $ (right) → reads "FUNDED ⟶ $X". Row 2: omni-search (left) + count (right). */
    .tbl-head tr { display:grid; grid-template-columns:auto 1fr 120px; row-gap:0.5rem; column-gap:0.5rem; padding:0.75rem 1rem; align-items:center; }
    .tbl-head .th-dot-cell    { grid-column:1; grid-row:1; }   /* col 2 FIXED 120px (badges) so the search (col 1) is uniform across all 3 tables. 16px sides = table cell inset (dot lines up with #). */
    .tbl-head td { display:block; height:auto; padding:0; }
    .tbl-head .th-title       { grid-column:2; grid-row:1; justify-self:start; padding-left:0; min-width:0; overflow:hidden; white-space:nowrap; }
    .tbl-head .th-amount      { grid-column:3; grid-row:1; justify-self:end; padding-left:0; }
    .tbl-head .th-search-cell { grid-column:1 / span 2; grid-row:2; justify-self:stretch; text-align:left; padding-left:0; }
    .tbl-head .th-search-cell .omni-wrap { display:flex; width:100%; }
    .tbl-head .th-search-cell input.th-search { width:100% !important; }
    .tbl-head .th-count-cell  { grid-column:3; grid-row:2; justify-self:end; text-align:right; padding-left:0; }
    /* Timestamp column (now col 5) off on mobile; the 5 remaining columns redistribute to fill so
       STATUS keeps room for its 105px badge. ST stays last at 10%. */
    .tracker-body col:nth-child(5) { width:0 !important; }
    .tracker-body th:nth-child(5), .tracker-body td:nth-child(5) { display:none !important; }
    .tracker-body col:nth-child(1) { width:6% !important; }
    .tracker-body col:nth-child(2) { width:23% !important; }
    .tracker-body col:nth-child(3) { width:23% !important; }
    .tracker-body col:nth-child(4) { width:38% !important; }
    .tracker-body col:nth-child(6) { width:10% !important; }
    /* right-anchor the STATUS/AMOUNT data badge to the same 16px inset as the count badge above (header
       stays centered via its own text-center class); measured header==body width, so this locks them. */
    .tracker-body td:nth-child(4) { text-align:right !important; }
    .tbl-head .th-amount-badge { width:120px !important; }   /* fit the fixed 120px track */
    .flip-container { height:480px !important; }   /* ~6 rows on mobile (taller 2-row header, so a bit less than desktop) */
}

::placeholder {
    color: var(--text-muted) !important;
    opacity: 1 !important;
}

/* Progress Shimmer */
@keyframes shimmer { 0%{transform:translateX(-100%)} 50%{transform:translateX(100%)} 100%{transform:translateX(100%)} }
.prog-fill::after { content:''; position: absolute; top:0; left:0; width:50%; height:100%; background: linear-gradient(to right,transparent,rgba(255,255,255,0.3),transparent); animation: shimmer 2.5s infinite linear; }

/* Slide-to-confirm motion cue — left-to-right sweep matching the drag direction */
.slide-shimmer-track::after { content:''; position: absolute; top:0; left:0; width:40%; height:100%; background: linear-gradient(to right,transparent,rgba(255,255,255,0.3),transparent); animation: shimmer 2.5s infinite linear; pointer-events: none; }
/* The slide gesture owns the touch — a drag must never pan/zoom the page (fixes "can't slide while
   zoomed" on mobile). Applies to every slide widget via the shared track class. */
.slide-shimmer-track, .slide-shimmer-track * { touch-action: none; }
/* Uniform drag handle — same grabbable bar the intent slider uses, shared by every confirm slider. */
.uni-handle { position:absolute; left:0; top:0; bottom:0; width:46px; display:flex; align-items:center; justify-content:center; background:var(--card-graphite); border-right:1px solid var(--border-silver); z-index:20; cursor:grab; box-shadow:2px 0 10px rgba(0,0,0,0.15); }
.uni-handle:active { cursor:grabbing; }
.uni-handle span { font-size:14px; font-weight:900; font-family:monospace; }

/* Gate confirm title — sized to guarantee one row even if the JetBrains Mono
   webfont hasn't loaded yet and falls back to a wider system monospace. */
.gate-confirm-title { font-size: 13px; letter-spacing: 0.3em; white-space: nowrap; }
@media (max-width: 480px) {
    .gate-confirm-title { font-size: 11px; letter-spacing: 0.2em; }
}

/* UX Attention Wave */
@keyframes wave-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
/* Chart TARGET line — a knife-edge highlight sweeps along it (same wave-shimmer as the personal/entity buttons).
   Base color set per-line via --tl; the bright band at 50% is the moving "blade". Positioned by drawLineWithAxes. */
.chart-target-line {
    position:absolute; height:2px; pointer-events:none; z-index:3; border-radius:1px; display:none;
    background-image: linear-gradient(90deg, var(--tl) 0%, var(--tl) 44%, rgba(255,255,255,0.95) 50%, var(--tl) 56%, var(--tl) 100%);
    background-size: 220% 100%;
    animation: wave-shimmer 8s infinite linear;
}
.attention-shimmer {
    background: linear-gradient(90deg, var(--card-graphite) 25%, var(--shimmer-wave) 50%, var(--card-graphite) 75%);
    background-size: 200% 100%;
    animation: wave-shimmer 2.5s infinite linear;
    border-color: var(--foundation-blue) !important;
    color: var(--foundation-blue) !important;
}

/* Ack rows: the text metrics are declared once here and renderAcks reads them, so the checkbox can
   derive its own centering. Bump --ack-fs later and the box re-centers itself on the first line —
   no re-tuning. Values are px/unitless on purpose: .inst-cb sets its own font-size:9px for the tick,
   so any em-based math written ON the box would resolve against 9px instead of the label's size. */
/* --ack-lh has a hard floor of 16/--ack-fs (1.231 at 13px): the "i" tooltip icon is a 16px inline-flex
   box, so a shorter line box would let it stretch whatever line it lands on. 1.25 keeps a small margin. */
#ack-list { --ack-fs: 13px; --ack-lh: 1.25; }
#ack-list .inst-cb { margin-top: calc((var(--ack-fs) * var(--ack-lh) - 14px) / 2); }

/* Consolidated from repeated inline styles (2026-08-21). Declarations are verbatim copies of what
   they replaced — every occurrence carried an identical style attribute, and none of the elements
   had a competing utility on the same properties, so this is a behaviour-preserving swap. */
.row-6  { display:flex; align-items:center; gap:6px; }
.row-10 { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
.dj-hint { font-weight:400; text-transform:none; opacity:0.65; }        /* the muted "→ …" note on a field label */
.dj-bullet { width:14px; flex-shrink:0; }


/* .kpi-clickable and .pie-clickable are set by wireRosterTriggers on the cards/pies that actually
   flip the roster — keying off them means the sweep can never appear on an inert card.
   ── HOVER EDGE ────────────────────────────────────────────────────────────────────────────────
   "There is something here" affordance for elements whose only current signal is the cursor turning
   into a hand. A hairline travels around the border on hover — .fx-edge for cards, .fx-edge-round
   for the pies.
   Built as a conic-gradient ring masked to the border box, so it follows ANY shape/radius without
   needing an SVG sized to the element. The angle is animated through an @property custom property,
   because a plain custom property is not interpolatable and would jump instead of sweep.
   Fallback: browsers without @property ignore the keyframes and get the static ring, which still
   reads as a highlight — so the affordance never disappears entirely. */
@property --fx-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.fx-edge, .fx-edge-round, .kpi-clickable { position: relative; }
.fx-edge::after, .fx-edge-round::after, .kpi-clickable::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    border-radius: inherit; padding: 1px;
    background: conic-gradient(from var(--fx-angle),
        transparent 0deg, transparent 200deg,
        color-mix(in srgb, var(--foundation-blue) 55%, transparent) 300deg,
        var(--foundation-blue) 340deg, transparent 360deg);
    /* mask to a 1px ring: fill the padding box, punch out the content box */
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    opacity: 0; transition: opacity .18s ease;
}
/* The pie's column is a rectangle, so inset:0 + 50% radius traced an ELLIPSE around the column
   instead of a ring around the donut. Size it to the column's height and centre it, so the ring is
   a true circle matching the drawn donut. */
.fx-edge-round::after { border-radius: 50%; inset: auto; transform: translate(-50%, -50%);
    width: var(--pie-d, 100%); height: var(--pie-d, 100%);
    left: var(--pie-cx, 50%); top: var(--pie-cy, 50%); }
.fx-edge:hover::after, .fx-edge-round:hover::after, .kpi-clickable:hover::after { opacity: 1; animation: fx-edge-run 1.4s linear infinite; }
@keyframes fx-edge-run { to { --fx-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) {
    .fx-edge:hover::after, .fx-edge-round:hover::after, .kpi-clickable:hover::after { animation: none; }   /* static ring, no sweep */
}

/* Checkbox specific override for portal */
.inst-cb { width: 14px; height: 14px; flex-shrink: 0; border: 1px solid var(--text-muted); border-radius: 2px; background: transparent; display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff; transition: all 0.15s; }
.inst-cb.on { background: var(--foundation-blue); border-color: var(--foundation-blue); }

/* Tooltip for Form Info — position is computed in JS (positionInfoTooltip) so it always
   stays clamped inside the host card, regardless of where the icon sits on the page. */
.info-tooltip-container { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border: 1px solid var(--border-silver); border-radius: 50%; cursor: help; margin-left: 6px; }
.info-tooltip-container:hover { border-color: var(--foundation-blue); color: var(--foundation-blue); }
.info-tooltip { position: fixed; background: var(--bg-obsidian); border: 1px solid var(--foundation-blue); padding: 12px; z-index: 100; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2); pointer-events: none; }
.info-tooltip-container:hover .info-tooltip, .info-tooltip-container:active .info-tooltip { opacity: 1; visibility: visible; }

/* ── CINEMATIC BOOT SEQUENCE ─────────────────────────────────────────────
   Always black background regardless of portal theme mode.
   Video plays with mix-blend-mode: screen so logo glows on void.
   ──────────────────────────────────────────────────────────────────────── */
#intro-overlay {
    position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
    background-color: #000000;
    z-index: 10000; display: flex; justify-content: center; align-items: center;
    flex-direction: column; transition: opacity 1s ease;
}
#intro-video {
    width: 100%; max-width: 900px; opacity: 0; transition: opacity 1.2s ease;
    pointer-events: none;
    mix-blend-mode: screen;
    -webkit-mask-image: radial-gradient(ellipse at center, black 50%, transparent 95%);
    mask-image: radial-gradient(ellipse at center, black 50%, transparent 95%);
}
#welcome-text {
    font-family: 'Libre Baskerville', serif; font-size: 1.4rem; color: #94a3b8;
    text-align: center; opacity: 0; transition: opacity 1s ease;
    position: absolute; padding: 20px; letter-spacing: 1px;
    font-style: italic; display: flex; flex-direction: column; align-items: center;
    pointer-events: none;
}
#bypass-clean, #bypass-gears {
    position: absolute; top: 0; width: 100px; height: 100px;
    z-index: 10001; pointer-events: auto; background: transparent; cursor: pointer;
}
#bypass-clean { left: 0; }
#bypass-gears { right: 0; }
.boot-fade-in  { opacity: 1 !important; }
.boot-fade-out { opacity: 0 !important; }
.boot-hidden   { display: none !important; }
.typing-line   { min-height: 2.2rem; margin: 4px 0; display: inline-block; }
.typing-active::after {
    content: '|'; display: inline-block;
    animation: blink-caret 0.75s step-end infinite;
    margin-left: 4px; color: #64748b; font-style: normal;
}
@keyframes blink-caret { from, to { opacity: 0; } 50% { opacity: 1; } }
@keyframes singularityBoot {
    0%, 20% { transform: scale(0); filter: brightness(35) blur(50px) contrast(15); opacity: 0; }
    70%      { transform: scale(1.08); filter: brightness(1.6) blur(4px) contrast(1.2); opacity: 1; }
    100%     { transform: scale(1); filter: brightness(1) blur(0px) contrast(1); opacity: 1; }
}

/* Audio Button */
.audio-btn {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--foundation-blue);
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    white-space: nowrap;
    transition: all 0.2s;
    flex-shrink: 0;
}
.audio-btn:hover { opacity: 0.7; }
.audio-btn.playing { color: var(--status-green); }

/* ============================================================
   DJ DECK CONSOLE — ALL 5 FRACTURES RESOLVED
   F1: Chassis height lock (overflow-y-auto inside fixed height)
   F2: Single column + live array.sort() reorder
   F3: Dark mode inheritance via CSS vars (already inherits)
   F4: Dual-sided flip toggle
   F5: Tactical Toolbar
   ============================================================ */
.dj-tab-bar { display:flex; gap:2px; padding:4px; background:var(--terminal-recess); border-bottom:1px solid var(--border-silver); overflow-x:auto; flex-shrink:0; }
.dj-tab-btn { font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:2px; color:var(--text-muted); background:transparent; border:none; padding:8px 16px; cursor:pointer; white-space:nowrap; transition:all 0.2s cubic-bezier(0.4,0,0.2,1); flex-shrink:0; border-radius:2px; }
.dj-tab-btn:hover:not(.dj-tab-active) { background-color:var(--nav-btn-hover); color:var(--foundation-blue); }
.dj-tab-btn.dj-tab-active { background-color:var(--nav-active-base); color:#fff; box-shadow:inset 0 0 0 1px var(--foundation-blue-hover), 0 1px 4px rgba(0,0,0,0.3); }

.dj-view { display:none; flex-direction:column; gap:24px; padding:24px 16px; }
.dj-view.dj-active { display:flex; }

/* entity-block — inherits CSS vars, automatically correct in light & dark */
.dj-entity { background:var(--card-graphite); border:1px solid var(--border-silver); padding:25px; display:flex; flex-direction:column; position:relative; transition:border-color 0.3s, box-shadow 0.3s; }
/* Deck cards get the same hover treatment as .nav-container: with 46 near-identical editors
   stacked in one column, a lit border shows which deck you are about to type into. No
   !important, so .dj-unsaved keeps its amber warning border while hovered. */
.dj-entity:hover { border-color:var(--foundation-blue); box-shadow:0 10px 20px -5px color-mix(in srgb, var(--foundation-blue) 15%, transparent); }
/* Console's main title bar — same treatment as the portal's .nav-container (0.2s), since it
   fills the same role. Keeps .blueprint-card's depth shadow and adds the glow on top, so the
   card doesn't flatten mid-hover. */
.dj-console-bar { transition:border-color 0.2s ease, box-shadow 0.2s ease; }
.dj-console-bar:hover { border-color:var(--foundation-blue); box-shadow:0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.05), 0 10px 20px -5px color-mix(in srgb, var(--foundation-blue) 15%, transparent); }
.dj-entity.dj-unsaved { border-color:var(--status-amber) !important; }
.dj-entity.dj-unsaved::after { content:'UNSAVED'; position:absolute; top:10px; right:70px; font-size:12px; font-weight:700; letter-spacing:2px; text-transform:uppercase; color:var(--status-amber); }

.dj-module-label { font-size:13px; color:var(--foundation-blue); text-transform:uppercase; margin-top:-25px; margin-bottom:16px; letter-spacing:2px; border-bottom:1px solid var(--border-silver); height:52px; box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; }

/* F1 — chassis body: fixed height, content scrolls inside */
.dj-deck-body { overflow-y:auto; border:1px solid var(--border-silver); background:var(--terminal-recess); padding:12px; margin-bottom:12px; }

/* inputs */
.dj-input { width:100%; background:var(--terminal-recess); border:1px solid var(--border-silver); color:var(--text-main); padding:8px; margin-bottom:10px; font-family:inherit; font-size:13px; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
.dj-input:focus { border-color:var(--foundation-blue); }
.dj-input::placeholder { color:var(--text-muted); }
/* Must sit AFTER .dj-input: it overrides that rule's padding/margin/width, and it replaced an
   inline style, which used to win by definition. */
.dj-input-inline { margin-bottom:0; flex:1; min-width:0; padding:8px 10px; }   /* .dj-input inside a flex row */
/* datetime-local calendar icon: the default glyph is dark and invisible on the dark console — brighten it. */
.dj-input::-webkit-calendar-picker-indicator { filter:invert(0.85); opacity:0.9; cursor:pointer; }
/* BCC select checkboxes — themed like .dj-input so they track light/dark instead of the browser's white default */
input.bcc-cb { vertical-align:middle; margin-right:6px; cursor:pointer; appearance:none; -webkit-appearance:none; width:14px; height:14px; background:var(--terminal-recess); border:1px solid var(--border-silver); border-radius:2px; cursor:pointer; position:relative; vertical-align:middle; }
input.bcc-cb:hover { border-color:var(--foundation-blue); }
input.bcc-cb:checked, input.bcc-cb:indeterminate { background:var(--foundation-blue); border-color:var(--foundation-blue); }
input.bcc-cb:checked::after { content:''; position:absolute; left:4px; top:1px; width:3px; height:7px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg); }
input.bcc-cb:indeterminate::after { content:''; position:absolute; left:2px; top:5px; width:8px; height:2px; background:#fff; }

/* textarea — overflow hidden, grows freely in ADMIN (chassis controls the portal side) */
.dj-textarea { overflow:hidden; min-height:120px; resize:vertical; line-height:1.6; font-size:13px; margin-bottom:10px; font-family:monospace; }

.dj-field-label { font-size:12px; color:var(--text-muted); text-transform:uppercase; letter-spacing:2px; font-weight:600; display:block; margin-bottom:5px; margin-top:6px; }
.dj-dual-col { display:grid; grid-template-columns:1fr 1fr; gap:16px; }

.dj-chassis-wrap { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
.dj-chassis-label { font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:2px; color:var(--text-muted); white-space:nowrap; flex-shrink:0; }
.dj-chassis-select { background:var(--terminal-recess); border:1px solid var(--border-silver); color:var(--foundation-blue); font-family:inherit; font-size:12px; font-weight:700; padding:8px 10px; cursor:pointer; outline:none; flex:1; -webkit-appearance:none; -moz-appearance:none; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 10px center; background-size:10px 6px; padding-right:28px; }
.dark-mode .dj-chassis-select { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }

.dj-select { -webkit-appearance:none; -moz-appearance:none; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 10px center; background-size:10px 6px; padding-right:28px !important; }
.dark-mode .dj-select { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }

/* Nav tabs are rendered once from the slot list, but desktop and mobile can order them differently.
   CSS order lets one button sit 4th on desktop and 6th on mobile without duplicating it in the DOM
   (which would mean duplicate IDs, and navigate() resolves tabs by ID). */
/* Per-tab colour + effect, driven from the console (navTabs). Colour is a custom property rather
   than a class per palette entry, because the console allows an arbitrary hex. */
/* Background always; text ONLY when a text colour was actually chosen. Defaulting this to #fff made
   every coloured tab force white text — invisible on any light fill in light mode, and !important
   meant it beat .nav-tab-active too. With no choice made, the tab keeps its normal text colour. */
.nav-btn.has-color { background-color: var(--tab-color) !important; }
.nav-btn.has-color:hover { background-color: var(--tab-color) !important; }
.nav-btn.has-color[style*="--tab-text"] { color: var(--tab-text) !important; }
/* Effects reuse the existing keyframes. .nav-fx-off is toggled by applyNavEffects() from tickClocks,
   so an effect can be tied to a deadline — e.g. APPLY stops pulsing the moment the window closes. */
.nav-fx-heartbeat { animation: heartbeat 1.8s ease-in-out infinite; }
.nav-fx-shimmer { background-image: linear-gradient(90deg, transparent 25%, var(--shimmer-wave) 50%, transparent 75%);
    background-size: 200% 100%; animation: wave-shimmer 2.5s infinite linear; }
/* Cancels a running effect. background-image is cleared ONLY for shimmer, which paints its sweep
   there — clearing it unconditionally also destroyed .nav-tab-active's gradient, leaving the active
   tab transparent (invisible in light mode, where the nav container is white). */
.nav-fx-off { animation: none !important; }
.nav-fx-shimmer.nav-fx-off { background-image: none !important; }

.nav-btn { order: var(--mb-order, 0); }
@media (min-width:1024px) { .nav-btn { order: var(--dt-order, 0); } }

/* Page dividers — binder tabs protruding from the BOTTOM edge of the nav box into the gap below it.
   Absolutely positioned so they hang OUTSIDE the bordered box while the box keeps its own height.
   This only works because #nav-wrapper's overflow-x-auto was removed (#nav-tabs owns horizontal
   scrolling now) — with overflow-x set, CSS forces overflow-y to auto and would clip them.
   .is-here = the page you're on (reads as continuous with the box, no top border).
   .has-active = the page holding the selected tab — browsing page 1 while APPLY sits on page 2 must
   not look like nothing is selected. */
/* #nav-pages is laid out in normal flow inside #nav-shell — see the markup. It must NOT be
   positioned here: absolute would make it report zero height and overlap the card below again. */
/* Geometry and colour are set inline at render time (renderNavPages) — the shared container owns all
   borders and the radius, so declaring any here just doubles up under it. */
/* Page dividers. Values here are exactly what renderNavPages used to set inline — moved back now
   that Cache-Control:no-cache on CSS means a change can't sit stale for an hour.
   .is-here paints a 1px band of card fill above itself so the card's bottom border breaks there and
   the chip reads as merged with the card; box-shadow because it costs no layout. */
/* Chart-card header — 52px, the same module .tracker-head / .tbl-head / .dj-section-head use.
   Height, not min-height: the card is `flex flex-col h-full` inside #mod-chart-nav's lg:h-[416px],
   with this row shrink-0 and the plot area flex-1, so the plot absorbs the difference and the card
   total never moves (header 33 -> 52, plot 339 -> 320, card stays 416).
   box-sizing:border-box, so the 52 already contains pb-2 (8px) and border-b (1px) -> 43px content.
   Unscoped on purpose: the class is desktop-authored but the row exists at every width, and
   scoping it to lg would change mobile, which is out of bounds. */
.chart-head { height: 52px; padding-left: 16px; padding-right: 16px; }
/* APM Splits is lifted to the h3's size: .text-label pins the h3 at 13px UNLAYERED, so the label
   beside it has to come up rather than the h3 come down. 0-2-0, so it beats `text-xs`'s 12px.
   The timeframe buttons are deliberately NOT in here. They used to be — held at 13px to make one
   type size across the header line, back when they were borderless .nav-btn text. They are .tf-pill
   now, so each carries a box, and a box reads heavier than bare text at the same size. They take
   .tf-pill's 11px, the console's size: a filter control should not carry the title's weight, and
   across 8 buttons the two points give back ~30px of header width. */
.chart-head .apm-splits { font-size: 13px; }

.nav-page-strip { display:flex; border:1px solid var(--foundation-blue); border-top:none; }
.nav-page-sep { width:1px; flex:0 0 1px; background:var(--foundation-blue); }
.nav-page-chip { min-width:22px; height:18px; padding:0 6px; flex:0 0 auto; border:none;
    display:inline-flex; align-items:center; justify-content:center;
    font-family:'JetBrains Mono',monospace; font-size:10px; font-weight:900; line-height:1;
    background:var(--terminal-recess); color:var(--text-muted);
    cursor:pointer; user-select:none; transition:color .12s, background .12s; }
.nav-page-chip.is-here { background:var(--card-graphite); color:var(--foundation-blue);
    box-shadow:0 -1px 0 0 var(--card-graphite); }
.nav-page-chip:hover { color:var(--foundation-blue); }
.nav-page-chip.has-active { color:var(--foundation-blue); }

/* Console clocks — METRICS / INTENT DEADLINE / FUNDING DEADLINE / SYSTEM TIMELINE. Borderless on
   purpose: the boxed treatment read as an editable form field, but these are readouts you switch.
   Pairs with .dj-select, which supplies the chevron and its padding-right:28px.
   background-color longhand, NEVER the `background` shorthand — the shorthand resets background-image
   and would silently delete .dj-select's chevron. Colour is owned by tickClocks (per mode). */
.dj-clock { width:100%; height:32px; padding-left:8px; padding-right:8px; background-color:transparent; border:0; border-radius:0; outline:none; font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; }

/* Tracker header: mobile = flex (title left; VIEW CHARTS + LIVE packed right).
   Desktop = same 2-col grid as the KPI grid below, so the button's left edge lands on the column-2 boundary. */
/* CARD-HEADER BASE — 52px title row, the module .dj-section-head already uses in the console.
   The portal is the anchor here, not MasterBuild: this class existed first and carried the layout,
   while height / border / shrink sat in the markup as h-[52px] border-b shrink-0 on 31 elements.
   Pulling them in makes one place to change the row instead of 31.
   padding is LONGHAND left/right on purpose — the shorthand would zero padding-top/bottom and, being
   unlayered, would beat the py-2 / py-3 mobile variants a few of these headers rely on.
   Modifiers stay on top: the tracker's desktop calc(50% - 6px) grid, #deadlines-card's 50/50,
   the cap table's min-w-[800px], the chart's flex-wrap. */
.tracker-head {
    display: flex; justify-content: space-between; align-items: center;
    padding-left: 16px; padding-right: 16px;
    height: 52px;
    border-bottom: 1px solid var(--border-silver);
    flex-shrink: 0;
    box-sizing: border-box;
}
@media (min-width:1024px) {
  .tracker-head { display:grid; grid-template-columns:calc(50% - 6px) calc(50% - 6px); gap:12px; align-items:center; padding-left:16px; padding-right:16px; }
  .tracker-head > .th-controls { justify-content:space-between; }
  /* Desktop: the grid track is already half the card. The inline width:calc(50%-6px)
     would resolve against that TRACK (→ ~quarter card) and collapse the two buttons on
     top of each other, so let the controls fill their track here. */
  #tracker-card .tracker-head > .th-controls { width:auto !important; }
  /* .th-flush — cap table / unit engine / transaction history / verifier hub / history hub. Their
     second child is a count badge + omni-search, and the 50/50 grid track above puts it in a track
     HALF the card wide, then ml-auto shrinks it to content and pushes it to the track's own right
     edge — leaving dead space between the grid gap and wherever the badge actually starts. That
     dead space is what turned "align this column with the badge" into a four-term derivation
     (title track + gap + dead space + badge) three times tonight instead of one constant.
     Reverting these to the SAME flex + justify-content:space-between the <1024px rule below already
     uses removes the track entirely: the badge+search block's right edge becomes a fixed
     `card width - 16px padding`, with nothing to derive — space-between fills the row directly,
     it does not create a second half to shrink-wrap inside of. */
  .tracker-head.th-flush { display: flex; justify-content: space-between; }

/* DECK CARD HEADER — title truncates, controls sit on the right edge less the padding.
   .tracker-head's desktop rule above is a 50/50 grid AND it is unlayered, so the flex/justify
   utilities written on the element never applied: the controls sat at the 50% line rather than the
   right edge, so FULL VIEW landed at a different x on every card, and a long title ran straight into
   the (AUDIO) button. 1fr auto fixes both by construction — one column that truncates, one exactly
   as wide as its content, and the gap keeps them apart whatever the title says.
   Same reasoning as the board leaderboard's grid-cols-[auto_1fr_auto]: share the edge, do not
   compute it. */
.tracker-head.deck-head { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; }

/* Only the VISION LETTER's header holds position. The ladder that was here — sticky on every card,
   +52 per card, z descending — is the console's construction, and it does not transfer: a console
   section head is a SIBLING of its body inside one card, so it is never evicted, while a portal
   header lives inside its own card and its card's bottom edge shoves it off the top. Blueprint's
   first card is 136px, so its head survived ~84px of scroll and then left a 52px hole with the next
   card's content showing through it. No portal view scrolls far enough for accumulation to be worth
   the artefacts.

   One pinned head is all the job needed: a 52px bar always on screen, so FULL VIEW / RETURN and the
   progress line are always reachable. Every other card scrolls as it always did.

   The card must not be overflow:hidden or an ancestor with overflow becomes the scrollport and the
   pinned header sits there looking static. 2px radius, nothing visible lost.

   border-bottom: none is why .dj-section-head's track reads thinner. The track is 2px of
   --border-silver at the head's bottom edge; leaving the border under it draws BOTH, 3px. The track
   IS the divider — that is the whole difference between the console's finesse and the heavy version. */
.deck-head { position: relative; }
.deck-head.is-pinned { position: sticky; top: 0; z-index: 5; }

/* The pinned header's z-index only ranks INSIDE its own card: .blueprint-card is
   position:relative;z-index:2, so every card is its own stacking context and a later sibling paints
   over the whole of an earlier one. The next card's content was scrolling ACROSS the pinned bar
   instead of beneath it. Lifting the card that owns the pinned header above its siblings is what
   actually orders them — the header's own z-index never could. */
.deck-deployed:has(> .deck-head.is-pinned) { z-index: 3; }
}
/* MOBILE tracker header: back to what it was before the desktop consolidation — dot + TRACKING on
   the left, the TABLES/CHARTS pair splitting the right half over FUND CAPITAL.
   .th-title carries display:grid inline for the desktop two-cell layout, so it needs !important to
   go back to a plain row; the toggle it holds is desktop-only here, and the two slot hosts are empty
   because VISION and BLUEPRINT are nav tabs at this breakpoint (mb.loc = 'nav'). Hidden rather than
   left in place so the pair gets .th-controls' two 1fr columns. */
@media (max-width:1023px) {
  #tracker-card .th-title { display:flex !important; align-items:center; gap:8px; min-width:0; }
  /* !important is load-bearing: both loc hosts carry an INLINE display:flex in the markup, and an
     inline declaration outranks any stylesheet rule short of !important — id specificity does not
     help. Without it they stayed as grid cells, so .th-controls became TWO rows (empty hosts, 12px
     gap, then the buttons) and the pair sat 17px down from the top of a 52px header against 5px
     below it. That is the "buttons sit low / uneven padding" report. */
  #tracker-card #chart-btn-toggle,
  #tracker-card #loc-trk-cap-l,
  #tracker-card #loc-trk-cap-r { display:none !important; }
}
/* The pair's active state, which used to be set from JS by id. Blue on the one you are looking at,
   both muted off the tracker — dash-on and charts-on are the same two classes the toggle reads. */
#chart-btn-tables, #chart-btn-charts { color: var(--text-muted); }
body.dash-on:not(.charts-on) #chart-btn-tables { color: var(--foundation-blue); }
body.dash-on.charts-on       #chart-btn-charts { color: var(--foundation-blue); }
/* Count cards: 2×2 on mobile, 4-across on desktop; tight side padding so the labels fit the quarter-width cards. */
/* Deadlines table col-2 (FUND POSITION / date) onto the same 50%+6 line as the KPI col-2 / VIEW CHARTS (its cell starts at 50%, so 6px of inset lands the text there). */
@media (min-width:1024px) { #deadlines-card th:nth-child(2), #deadlines-card td:nth-child(2) { padding-left:6px !important; } }
.kpi-mini { padding:8px; }
.kpi-mini.kpi-clickable { transition: transform 0.1s cubic-bezier(0.4,0,0.2,1); }
.kpi-mini.kpi-clickable:active { transform: translateY(2px); }
.pie-clickable { transition: transform 0.1s cubic-bezier(0.4,0,0.2,1); }
.pie-clickable:active { transform: translateY(2px); }
.kpi-mini .text-label { letter-spacing:0.03em; }   /* KPI card titles: tighter than the default 0.1em text-label */
/* Member KPI outer split (counts | dollars). MOBILE only: 52/48 so PERSONAL fits; desktop (>=1024) stays 50/50 = identical to the old grid-cols-2. gap 12px → -6px per col. */
/* MOBILE: left counts become 4 stacked HORIZONTAL rows at full width (label left, number right) so labels never wrap; desktop keeps the vertical 2x2, untouched. Text sizes unchanged. */
.kpi-counts { display:grid; grid-template-columns:1fr; gap:12px; }
@media (min-width:1024px) { .kpi-counts { grid-template-columns:minmax(0,1fr) minmax(0,1fr); } }
@media (max-width:1023px) {
    /* each count row: label + sub stacked left, number vertically centered right (normal size); fixed 44px so 2 rows + gap = one dollar box */
    .kpi-counts .kpi-mini { display:grid; grid-template-columns:1fr auto; grid-template-rows:22px 22px; align-items:center; column-gap:8px; height:44px; padding-top:0; padding-bottom:0; }
    .kpi-counts .kpi-mini > :nth-child(1) { grid-column:1; grid-row:1; }
    .kpi-counts .kpi-mini > :nth-child(3) { grid-column:1; grid-row:2; }
    .kpi-counts .kpi-mini > :nth-child(2) { grid-column:2; grid-row:1 / span 2; align-self:center; justify-self:center; }
    /* mobile order: PERSONAL, ENTITY, FUNDED, OPEN — both intents together up top; OPEN last (last to die); HTML order is PERSONAL, OPEN, ENTITY, FUNDED */
    .kpi-counts > :nth-child(1) { order:1; }   /* PERSONAL */
    .kpi-counts > :nth-child(3) { order:2; }   /* ENTITY */
    .kpi-counts > :nth-child(4) { order:3; }   /* FUNDED */
    .kpi-counts > :nth-child(2) { order:4; }   /* OPEN */
    /* money card = table rows registering with the left column: label wraps to 2 lines in a 44px block, 12px gap, then $ + sub in a 44px block — mirrors left card 44 + gap 12 + card 44. $ drops to title size/font so it sits on a line. */
    .kpi-dollars > div { height:100px; display:grid; grid-template-rows:22px 22px 12px 22px 22px; padding:0 8px; }
    .kpi-dollars > div > :nth-child(1) { grid-row:1 / span 2; display:block; max-width:72px; white-space:normal; line-height:22px; }   /* CAPITAL / GOAL — 22px lines drop into rows 1 & 2 = PERSONAL / INTENTS lines */
    .kpi-dollars > div > :nth-child(2) { grid-row:4; align-self:center; font-size:13px !important; font-family:inherit; font-weight:700; }   /* $ on row 4 = ENTITY line; title size + font + weight */
    .kpi-dollars > div > :nth-child(3) { grid-row:5; align-self:center; }                                                              /* sub on row 5 = ENTITY sub line */
    .kpi-disclaimer { display:none; }      /* mobile: reclaim vertical space */
    /* trim the mobile vertical gaps between the big cards. #nav-wrapper no longer owns the menu →
       deadline gap — #nav-shell does, and its padding-bottom is reserved for the page dividers, so
       that one is set inline in the markup. */
    .kpi-pillar { gap:12px !important; }    /* deadline → tracker (and pillar cards): 16px → 12px */
    .terminal-grid { gap:12px !important; } /* !important: beats the gap-4 utility, which loads later */
    /* KPI → meters gap = the 12px inter-KPI gap: drop justify-between's slack, fit card to content */
    #tracker-card { height:auto; justify-content:flex-start; }
    /* 2px off the tracker body's bottom padding (py-3 = 12px -> 10px). !important beats the utility,
       which loads after this file. */
    #tracker-card .tracker-kpi-body { padding-bottom:10px !important; }
    #deadlines-card th { letter-spacing:0.03em; }   /* rein in SUBMIT INTENT / FUND POSITION so the longer ES strings fit the 50% column */
}

/* Deadlines header geometry — ALL widths, not just mobile. It must match the table beneath it, and
   the table is .finesse-table (table-layout:fixed; width:100%) with two auto columns, i.e. a hard
   50/50 across the card's full inner width, each cell inset 16px by the th's !px-4.
   The generic desktop .tracker-head is a different geometry — 16px padding on the HEAD plus
   calc(50% - 6px) columns and a 12px gap — which puts col 2's text at 0.5*inner + 6 against the
   table's 0.5*inner + 16. That 10px is why WINDOW OPEN/CLOSED sat left of FUND POSITION on desktop.
   Id specificity beats .tracker-head, so the tracker card keeps the generic rule. */
#deadlines-card .tracker-head { display:grid; grid-template-columns:50% 50%; gap:0; padding-left:0; padding-right:0; }
#deadlines-card .tracker-head .th-title { padding-left:16px; }
#deadlines-card .tracker-head .th-controls { padding-left:16px; }
.kpi-mini-sub { font-size:11px; line-height:1rem; }
.tf-pill { font-size:11px; font-weight:900; letter-spacing:0.1em; text-transform:uppercase; padding:3px 6px; border:1px solid var(--border-silver); color:var(--text-muted); background:transparent; cursor:pointer; border-radius:2px; transition:color 0.1s,border-color 0.1s,background 0.1s; line-height:1; user-select:none; }
.tf-pill:hover { color:var(--foundation-blue); border-color:var(--foundation-blue); }
.tf-pill:active { transform:translateY(2px); }
.tf-pill.tf-active { color:var(--foundation-blue); border-color:var(--foundation-blue); background:rgba(59,130,246,0.1); }
.dj-switch-row { display:flex; align-items:center; gap:15px; flex-wrap:wrap; margin-top:10px; margin-bottom:10px; }
.dj-switch-wrap { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-muted); font-weight:600; text-transform:uppercase; letter-spacing:1px; cursor:pointer; }
.dj-speed-slider { -webkit-appearance:none; appearance:none; width:100%; height:4px; border-radius:2px; background:var(--border-silver); cursor:pointer; outline:none; --fill:15%; }
.dj-speed-slider::-webkit-slider-runnable-track { height:4px; border-radius:2px; background:linear-gradient(to right, var(--foundation-blue) var(--fill), var(--border-silver) var(--fill)); }
.dj-speed-slider::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%; background:var(--foundation-blue); border:2px solid var(--bg-obsidian); margin-top:-5px; cursor:pointer; }
.dj-speed-slider::-moz-range-track { height:4px; border-radius:2px; background:var(--border-silver); }
.dj-speed-slider::-moz-range-progress { height:4px; border-radius:2px; background:var(--foundation-blue); }
.dj-speed-slider::-moz-range-thumb { width:14px; height:14px; border-radius:50%; background:var(--foundation-blue); border:2px solid var(--bg-obsidian); cursor:pointer; }
.dj-speed-slider::-webkit-slider-thumb { box-shadow:0 0 9px var(--foundation-blue); transition:box-shadow .15s, transform .1s; }
.dj-speed-slider:active::-webkit-slider-thumb { transform:scale(1.15); box-shadow:0 0 14px var(--foundation-blue); }
.dj-speed-slider::-moz-range-thumb { box-shadow:0 0 9px var(--foundation-blue); }
.dj-speed-slider:active::-moz-range-thumb { box-shadow:0 0 14px var(--foundation-blue); }
.dj-speed-bubble { position:absolute; top:-10px; transform:translateX(-50%); font-family:monospace; font-weight:900; font-size:11px; color:var(--status-amber); pointer-events:none; text-shadow:0 0 6px var(--status-amber); }
.dj-speed-step { flex:none; width:22px; height:22px; display:inline-flex; align-items:center; justify-content:center; padding:0; background:var(--bg-obsidian); border:1px solid var(--border-silver); border-radius:4px; color:var(--status-amber); font-family:monospace; font-size:15px; font-weight:900; line-height:1; cursor:pointer; user-select:none; transition:border-color .12s, background .12s; }
.dj-speed-step:hover { border-color:var(--foundation-blue); background:var(--bg-graphite, var(--bg-obsidian)); }
.dj-speed-step:active { transform:scale(.9); }
.dj-color-swatch { flex:none; width:32px; height:32px; padding:2px; background:var(--terminal-recess); border:1px solid var(--border-silver); border-radius:2px; cursor:pointer; }
.dj-color-swatch::-webkit-color-swatch-wrapper { padding:0; }
.dj-color-swatch::-webkit-color-swatch { border:none; border-radius:1px; }
.dj-color-swatch::-moz-color-swatch { border:none; border-radius:1px; }

/* Portal launch-clock dropdown (subtitle row). Mirrors the console's clock selects, but custom-built:
   a native <select> would force the row to a 32px box and shift the header controls. */
.nav-launch-opt { padding:6px 8px; color:var(--text-muted); cursor:pointer; white-space:nowrap; transition:background .12s, color .12s; }
.nav-launch-opt:hover { background:var(--card-graphite); color:var(--foundation-blue); }
.nav-launch-opt.is-active { color:var(--foundation-blue); }

.css-marquee { display:inline-block; white-space:nowrap; flex-shrink:0; will-change:transform; }
#nav-marquee-wrap > div { display:flex; align-items:center; height:100%; }   /* vertically center the scrolling text in the marquee strip */
#nav-marquee-preview-wrap > div { display:flex; align-items:center; height:100%; }   /* same, for the console's live preview strip */
/* MOBILE charts: show one chart at a time, swap via chevrons (locked, not scroll) */
@media (max-width:1023px){
    body.charts-on.chartpage-intent #funded-flip-container { display:none; }
    body.charts-on.chartpage-funded  #intent-flip-container { display:none; }
}
.chart-chev-down,.chart-chev-up { cursor:pointer; user-select:none; line-height:1; }
.chart-chev-down { animation:chevDown 1.4s ease-in-out infinite; }
.chart-chev-up   { animation:chevUp   1.4s ease-in-out infinite; }
@keyframes chevDown { 0%,100%{transform:translateY(0);opacity:.6;} 50%{transform:translateY(3px);opacity:1;} }
@keyframes chevUp   { 0%,100%{transform:rotate(180deg) translateY(0);opacity:.6;} 50%{transform:rotate(180deg) translateY(3px);opacity:1;} }
/* roster title-bar alert pill: glow pulse when a new access request or intent lands */
@keyframes alertFlash { 0%,100%{ box-shadow:0 0 0 0 rgba(245,158,11,0); } 50%{ box-shadow:0 0 12px 2px var(--status-amber); } }
.alert-flash { animation: alertFlash 0.6s ease-in-out 3; }

/* Rendered as a CHECKBOX (matches the SLOT-directive .bcc-cb) — .active = checked. Same .active wiring as
   before (bootToggle / djSwitch / toggleMarqueeColorOverride just flip .active), so no handler/save-load change. */
.dj-kill-switch { width:14px; height:14px; background:var(--terminal-recess); border:1px solid var(--border-silver); border-radius:2px; position:relative; cursor:pointer; transition:background .2s,border-color .2s; display:inline-block; flex-shrink:0; vertical-align:middle; }
.dj-kill-switch:hover { border-color:var(--foundation-blue); }
.dj-kill-switch.active { background:var(--foundation-blue); border-color:var(--foundation-blue); }
.dj-kill-switch.active::after { content:''; position:absolute; left:4px; top:1px; width:3px; height:7px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg); }
.dj-kill-switch.remove.active { background:var(--status-red); border-color:var(--status-red); }
.dj-kill-switch.ghost.active { background:var(--text-muted); border-color:var(--text-muted); }
/* Reusable labeled toggle-button — 36px to match the input boxes; shows its live state word via data-off/data-on.
   bootToggle() just flips .active; CSS swaps the word. Candidate to replace .dj-kill-switch console-wide. */
/* Toggle hover — the transition above was declared for a hover that never existed.
   Inactive mirrors .dj-toolbar-btn:hover; active mirrors .btn-standard:hover. */

.dj-status { font-size:12px; color:var(--text-muted); margin-top:6px; }
.dj-status span { color:var(--foundation-blue); }

/* F2 — single column, sort engine repaints this */
.dj-entity-grid { display:flex; flex-direction:column; gap:16px; }

/* Responsive 2-column grid for small entity cards (IDENTITY, CHROMA) */
.dj-entity-grid-2col { display:grid; grid-template-columns:1fr; gap:16px; }
@media (min-width: 768px) { .dj-entity-grid-2col { grid-template-columns:1fr 1fr; } }

/* PORTAL SHELL — #admin-console's construction. At lg the shell is fixed to the viewport and owns the
   scroll, so #app-header's sticky top-0 pins against a scroller that cannot itself move. That is the
   difference between the console bar and what the portal had: the header was sticky against the
   DOCUMENT, and the document was the thing sliding.
   The JS that scrolls has to follow — portalScroller() reads position:fixed off this rule rather than
   duplicating the 1024 constant, so the breakpoint stays here and nowhere else.
   Below 1024 this is a plain static div and the document scrolls as before. Mobile is untouched. */

/* HEADER READS AS A CARD. The console title bar is a blueprint-card on the graphite surface; this
   gives #app-header the same face without wrapping anything — no extra element, no forced height.
   The first attempt wrapped both header rows (title AND marquee) in a card pinned to 76px and put
   height:100% on every child, so the two rows stacked into the same 76px and the header collapsed.
   The card look was only ever the surface colour; nothing structural was needed.
   Borderless: on a graphite surface a border just draws a box around something already reading as
   one. Desktop only — below 1024 the title wraps to two lines and the header has to breathe. */
/* ONE STICKY BLOCK, NOT A LADDER. #app-header holds the title row, the marquee and the nav menu, so
   all three pin together at top:0 and cannot drift apart, overlap, or open a transparent gap — there
   is no offset between them to keep in sync, because there is no gap between them at all.
   NO height here. The block is deliberately content-sized: the desktop nav grows a page-divider
   strip whenever renderNavPages splits it across pages, and the mobile title wraps to two lines.
   Pinning a height would clip both. Everything below instead offsets by --header-h, which
   syncHeaderVar() writes from this element's own offsetHeight — so the height and the offset are
   the SAME measurement and cannot desync. That is the one property the console's construction
   lacks: it hand-copies its 59 into 20+ inline `top:` styles.
   Graphite is scoped to .hdr-face (the title row) rather than set here, so the marquee and nav keep
   sitting on obsidian exactly as they did in the columns they came from. */
@media (min-width: 1024px) {
    #app-header .hdr-face { background: var(--card-graphite); border: none; }
}

/* PILLAR DRAWER — the 505 track closes so tables/charts get the whole pane (558 -> 1079).
   NO transition, deliberately. The animated version made revealCharts() — which fires on a 120ms
   timer in _setTrackerState — measure the pane mid-slide and lock a short chart height in. Instant
   means the width is final before that timer runs and nothing downstream needs retiming. If the
   slide is ever wanted back, it is this transition PLUS moving that timer past its duration.
   Track to 0 rather than display:none on .kpi-pillar: none would drop it out of layout entirely,
   and .kpi-pillar's existing lg:overflow-hidden already clips the content as the track closes.
   gap goes with it or a dead 16px seam is left behind. */
@media (min-width: 1024px) {
    body.pillar-collapsed .terminal-grid { grid-template-columns: 0px 1fr; gap: 0; }
}



/* DESKTOP-ONLY FULL WIDTH (body.dt-fullwidth, set by navigate for DT_FULLWIDTH_VIEWS).
   NEWS and BOARD live inside .terminal-grid's second column, so they cannot use the hidden-view
   trick the engine views use — that would hide them along with the grid. Collapse the grid to one
   track and drop the pillar, and the view takes the whole 1195.
   Body class rather than a width check in JS: the breakpoint stays in the stylesheet and a resize
   needs no listener. Below 1024 nothing here applies, so mobile keeps deadlines, tracker and intent
   on these tabs exactly as before. Beats the lg:grid-cols-[505px_1fr] utility because this file is
   unlayered and Tailwind v4 utilities sit in @layer utilities. */
@media (min-width: 1024px) {
    body.dt-fullwidth .terminal-grid { grid-template-columns: 1fr; }
    body.dt-fullwidth .kpi-pillar { display: none; }
}

/* BUTTON DIRECTIVE — one tab is two cards, desktop left and mobile right, each column stacking on
   its own. align-items:start so a tall desktop card never stretches the mobile card beside it. */
.dj-tabs-2col { display:grid; grid-template-columns:1fr; gap:16px; align-items:start; }
@media (min-width: 768px) { .dj-tabs-2col { grid-template-columns:1fr 1fr; } }
.dj-tabs-col-head { font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:900; letter-spacing:0.1em; color:var(--foundation-blue); text-transform:uppercase; border-bottom:1px solid var(--border-silver); padding-bottom:6px; margin-bottom:8px; }
/* Position field — the card's ORDER, sitting with the movers instead of buried in the body. It is
   the same input djCollectTabs publishes, moved rather than duplicated. */
.dj-pos-input { width:34px; height:22px; text-align:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:900; color:var(--foundation-blue); background:var(--terminal-recess); border:1px solid var(--border-silver); border-radius:4px; padding:0; }
.dj-pos-input:focus { outline:none; border-color:var(--foundation-blue); }
.dj-pos-input::-webkit-outer-spin-button, .dj-pos-input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
/* Card movers — same footprint as the × they sit beside, so the three read as one control group. */
.dj-move-btn { width:22px; height:22px; display:flex; align-items:center; justify-content:center; border:1px solid var(--border-silver); border-radius:4px; background:var(--bg-obsidian); color:var(--foundation-blue); font-size:9px; line-height:1; cursor:pointer; padding:0; }
.dj-move-btn:hover:not(:disabled) { border-color:var(--foundation-blue); }
.dj-move-btn:disabled { opacity:0.25; cursor:default; }


/* Responsive 4-column grid for compact entity cards (CHROMA color families) */
.dj-entity-grid-4col { display:grid; grid-template-columns:1fr; gap:16px; }
@media (min-width: 768px) { .dj-entity-grid-4col { grid-template-columns:1fr 1fr; } }
@media (min-width: 1280px) { .dj-entity-grid-4col { grid-template-columns:1fr 1fr 1fr 1fr; } }

/* Console sub-tab bar — sticky below main bar, only visible on CONTENT panel */
.dj-console-tab-bar-wrap { display:none; background:var(--bg-obsidian); border:1px solid var(--border-silver); border-radius:2px; padding:0 16px; }
.dj-console-tab-bar-wrap.visible { display:flex; align-items:center; height:52px; box-sizing:border-box; }
.dj-console-tab-bar-wrap .dj-tab-bar { padding-left:0; padding-right:0; background:transparent; border:none; }

.dj-section-head { font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:2px; color:var(--status-amber); height:52px; box-sizing:border-box; margin-top:-24px; margin-bottom:4px; display:flex; align-items:center; justify-content:space-between; cursor:pointer; }
.dj-section-chevron { transition:transform 0.2s; }

.dj-stack-validator { display:flex; align-items:center; gap:12px; padding:10px 14px; background:var(--terminal-recess); border:1px solid var(--border-silver); margin-top:8px; }
.dj-stack-label { font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:2px; color:var(--text-muted); white-space:nowrap; }
.dj-stack-total { font-size:13px; font-weight:700; transition:color 0.3s; }
.dj-stack-bar { flex:1; height:4px; background:var(--border-silver); overflow:hidden; }
.dj-stack-fill { height:100%; transition:width 0.3s, background 0.3s; }

.dj-transmit-btn { padding:10px 25px; background:var(--bg-obsidian); color:var(--status-green); font-size:13px; letter-spacing:3px; text-transform:uppercase; cursor:pointer; border:1px solid var(--status-green); transition:all 0.3s; font-family:inherit; }
.dj-transmit-btn:hover { background:var(--status-green); color:var(--bg-obsidian); }

.dj-chroma-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; align-items:end; }
.dj-chroma-swatch { width:16px; height:16px; border:1px solid var(--border-silver); display:inline-block; vertical-align:middle; margin-right:8px; flex-shrink:0; }

.dj-footer-bar { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 24px; border-top:1px solid var(--border-silver); flex-wrap:wrap; }

.dj-deploy-btn { padding:8px 16px; color:var(--foundation-blue); border:1px solid var(--foundation-blue); font-size:12px; letter-spacing:2px; cursor:pointer; transition:all 0.3s; background:transparent; font-family:inherit; text-transform:uppercase; font-weight:700; }
.dj-deploy-btn:hover { background:var(--foundation-blue); color:#fff; }
/* ── THE PORTAL SCROLLS INSIDE ITSELF ───────────────────────────────────────────────────────────
   #admin-console is `fixed inset-0 … overflow-y-auto`: it IS the viewport and scrolls internally, so
   the document never scrolls behind it and it never grows a page scrollbar. The portal had the other
   shape — #app-main was a normal block in flow, its height grew the document, and the BODY scrolled.
   That is the structural difference, and it is why content at the end of one column could put a
   scrollbar on the whole page. Same shape as the console now: the header keeps its own height and
   #app-main takes exactly what is left, scrolling inside.

   100dvh, not 100vh — on mobile the URL bar collapses and vh keeps the taller value, which would
   leave a strip of main permanently below the fold. --header-h is published by setHeaderHeight() from
   #app-header.offsetHeight on load and on every resize/zoom settle, so this tracks a header that
   GROWS: the ES nav wraps to a second row and a fixed number would clip it.

   lg and up only. Below that the stacked layout is far taller than the viewport and document scroll
   is what a phone expects — an internal scroll container fights the browser's own chrome-hiding.

   Scoped to #app-main, so the gate screen and the print record — both also children of <body> —
   keep normal document flow.

   body:not(.force-stack) IS NOT OPTIONAL, same as the note at :207. The masthead double-tap stacks
   the portal at ANY width — #nav-band goes single-column, so the header alone can outgrow the
   viewport. Pinning body to 100dvh there clipped it (the nav tabs disappeared) and left flex:1 with
   nothing to give main. Stacked mode is deliberately one long scrolling page; it keeps document
   scroll, and the sticky offsets below keep their --header-h for the same reason. */
@media (min-width: 1024px) {
    /* <body> is already `flex flex-col`. Pin it to the viewport and let #app-main FLEX into whatever
       is left, rather than computing `100dvh - var(--header-h)`. The computed form left a few pixels
       of residual scroll, and it always would: --header-h is #app-header.offsetHeight, an integer
       reading of one element, so it cannot account for a fractional height or for anything else that
       sits between the header and main. flex:1 measures nothing and cannot be a few pixels wrong.
       Same lesson as the leaderboard's pl-[38px] — a constraint holds where arithmetic drifts.

       :has() scopes it to when the portal is actually on screen, so the gate and the print record —
       both also children of <body> — keep normal flow. Entering the console adds .hidden-view to
       #app-main, which releases this too. */
    body:not(.force-stack):has(> #app-main:not(.hidden-view)) {
        height: 100dvh;
        overflow: hidden;
    }
    body:not(.force-stack) #app-main:not(.hidden-view) {
        flex: 1 1 auto;
        min-height: 0;                  /* a flex item's default min-height:auto refuses to shrink */
        overflow-y: auto;
        overscroll-behavior: contain;   /* stop a bottomed-out scroll chaining to the document */
    }

    /* STICKY OFFSETS MEASURE FROM THE SCROLLER, AND THE SCROLLER CHANGED.
       #charts-column and .kpi-pillar are `sticky top-[var(--header-h)]`. That was right while the
       DOCUMENT scrolled: the header floated over the content, so a sticky column had to clear it.
       #app-main is the scroller now and the header sits OUTSIDE it, so main's top edge is already
       below the header — keeping the offset pushes both columns down by a full header height and
       opens an empty band above the NAV chart. Measured from this scroller the right offset is 0.

       #nav-shell keeps its own offset: it lives inside #app-header, not in here.

       Two ids, and unlayered, so it beats the Tailwind utility either way — the same reason
       voting.css's flip-height rules are written where they are. */
    body:not(.force-stack) #app-main #charts-column,
    body:not(.force-stack) #app-main .kpi-pillar { top: 0; }

    /* A DECLARED HEIGHT NEEDS flex:none, OR IT IS A SUGGESTION.
       #right-view-container is `flex flex-col`, and its children are the terminal grid, the views
       (lg:h-[1068px]) and the footer. While #app-main grew to fit its content this never showed: the
       container was as tall as it needed to be. Bounding main to the viewport made the container
       1165px against 1068 + ~137 of footer = 1205 wanted, so flex did what flex does and shrank the
       view — measured 1061 instead of 1068. The view is overflow:hidden, so those 7px took the last
       card's bottom border with them, on every tab whose content actually reaches the bottom.

       The height is the spec; the container is not allowed to negotiate it. Overflow past the
       viewport is #app-main's job to scroll, which is exactly what it is now for. */
    #right-view-container > * { flex-shrink: 0; }
}

/* ── CLICK TARGETS THAT ARE TEXT ────────────────────────────────────────────────────────────────
   Anything you click more than once in a row highlights its own label on the second click, which
   turns every one of these into a smear of selected text. Worst on the click-COUNT gestures —
   triple-click the console title to peek at the portal, triple-click a PHONE / EMAIL header to redact
   just that column, double-click the masthead to hold the stacked view — but a section head you
   collapse and expand a few times reads the same way.

   Not a new idea in this file, just an incomplete one: .tactile-btn (:578), .dj-deck-chev (:1705),
   .chart-chev-* (:1570) and the #gate-title / #nav-fund-title / #ops-console-title trio already carry
   it. These are the rest of the same family.

   Card BODIES are deliberately excluded — a member copying a paragraph out of a deck is doing
   something reasonable, and a flip card's copy stays selectable.

   Matched by ELEMENT where it is safe to: a th, a heading or a bare span carrying an onclick is a
   control, never prose. div[onclick] is deliberately NOT in that list — .flip-container is a div, and
   a seat card's description has to stay selectable. The div-shaped chrome is named instead. */
th[onclick], span[onclick], label[onclick],
h1[onclick], h2[onclick], h3[onclick], h4[onclick],
.dj-section-head, .dj-sub-label, .dj-switch-wrap, .dj-tab-btn,
.dj-kill-switch, .nav-launch-opt, #nav-launch,
th.cursor-pointer { -webkit-user-select: none; user-select: none; }

/* Per-deck collapse chevron (injected into each .dj-module-label) — clear a deck out of view to work on others */
.dj-deck-chev { cursor:pointer; margin-right:8px; display:inline-block; transition:transform .2s; color:var(--text-muted); user-select:none; }
.dj-deck-chev:hover { color:var(--foundation-blue); }
.dj-entity.dj-collapsed > .dj-module-label > .dj-deck-chev { transform:rotate(-90deg); }
.dj-entity.dj-collapsed > *:not(.dj-module-label) { display:none; }
/* Ack sub-group headers (PERSONAL / ENTITY): collapse their own group only. Hidden when the whole
   module is collapsed, so folding ACKNOWLEDGMENTS doesn't leave three orphan labels stacked up. */
.dj-sub-label { cursor:pointer; }
.dj-sub-label.is-collapsed .dj-deck-chev { transform:rotate(-90deg); }
.dj-entity.dj-collapsed > .dj-sub-label { display:none; }

/* NAV-BUTTON ROW COLLAPSE — the same affordance as .dj-entity.dj-collapsed above, applied to the
   BUTTON DIRECTIVE's rows. Eleven slots render a DESKTOP and a MOBILE card each, so editing the ninth
   otherwise means scrolling past eight.
   .dj-tab-controls is spared: [order] [up] [down] [x] stay reachable while a row is collapsed, which
   is what makes a collapsed column usable for reordering rather than just shorter.
   .dj-tab-label carries the slot's own name, mirrored from DT EN by djSyncTabLabel — without it every
   collapsed row reads DESKTOP 1 / DESKTOP 2 and the stack is an index you cannot read. */
.dj-tab-row.dj-collapsed > *:not(.dj-tab-num):not(.dj-tab-controls) { display:none; }
.dj-tab-row.dj-collapsed > .dj-tab-num { margin-bottom:0; }
.dj-tab-row.dj-collapsed .dj-deck-chev { transform:rotate(-90deg); }
.dj-tab-num { cursor:pointer; -webkit-user-select:none; user-select:none; }
.dj-tab-label { color:var(--text-muted); font-weight:700; letter-spacing:0.06em; }
.dj-tabs-allbtns { float:right; display:inline-flex; gap:10px; }
.dj-tabs-allbtns .dj-remove-deck-btn { font-size:10px; }
.dj-remove-deck-btn { font-size:12px; font-weight:700; color:var(--text-muted); background:transparent; border:none; cursor:pointer; letter-spacing:1px; text-transform:uppercase; transition:color 0.2s; padding:0; }
.dj-remove-deck-btn:hover { color:var(--status-red); }
/* ── CONSOLE-DEPLOYED CARDS ───────────────────────────────────────────────────────────────────────
   A card deployed from the console gets its height from its CHASSIS, which is a runtime number out of
   Firestore. That cannot be a Tailwind arbitrary value — those compile from source text, so `h-[${n}px]`
   builds nothing (ARCHITECTURE.md §7.6). It arrives as --deck-h inline instead.

   The compound `.flip-container.deck-deployed` is not decoration. styles.css:40 sets
   `.flip-container { height: 100% }` UNLAYERED, and an unlayered rule beats any Tailwind utility
   regardless of specificity — the trap voting.css:56-79 documents at length, and the exact bug that
   made the seat cards each fill their whole pane instead of stacking. 0,2,0 beats that 0,1,0.

   Flat cards mirror the built-ins: free height on mobile, declared from 1024 up. Flip cards take the
   declared height at every width, because .flipper/.front/.back are all height:100% and would collapse
   to nothing against an auto parent. */
.deck-deployed { height: auto; }
.flip-container.deck-deployed { height: var(--deck-h, 268px); }
@media (min-width: 1024px) { .deck-deployed { height: var(--deck-h, 268px); } }

/* DELETE, injected into every card editor's header by _initDeckChevrons. margin-left:auto rather than
   a justify-content change on .dj-module-label: that label is flex-start for the six other things it
   carries, and auto puts DELETE on the right edge whatever else is in there — a bare title span on the
   fixed decks, a title plus the ORDER group on a deployed one. */
.dj-delete-card-btn { margin-left:auto; }
/* Exit / abort actions: letters go red on hover to signal leaving. Custom CSS because
   hover:text-[var(--status-red)] is NOT in the purged tailwind build. */
.btn-exit:hover { color:var(--status-red) !important; }

/* F4 — dual-sided back panel */
.dj-back-panel { border-top:1px dashed var(--border-silver); margin-top:12px; padding-top:12px; }
.dj-back-label { font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:2px; color:var(--status-purple); margin-bottom:8px; display:block; }

/* F5 — tactical toolbar */
.dj-toolbar { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; padding:8px; background:var(--bg-obsidian); border:1px solid var(--border-silver); }
.dj-toolbar-btn { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:1px; color:var(--text-muted); background:transparent; border:1px solid var(--border-silver); padding:4px 8px; cursor:pointer; transition:all 0.2s; font-family:inherit; white-space:nowrap; }
.dj-toolbar-btn:hover { color:var(--foundation-blue); border-color:var(--foundation-blue); }

/* sort order input compact */
.dj-sort-input { width:60px !important; display:inline-block; text-align:center; padding:6px 8px !important; font-size:12px !important; margin-bottom:0 !important; }

@media print {
    body { background: white !important; color: black !important; margin: 0; padding: 0; }
    /* The portal's viewport lock must not survive into print. A 100dvh body with overflow:hidden
       clips a multi-page record to its first page, and `body > * { display:none }` below does not
       release it — :has() matches on the element existing, not on it being displayed. */
    body { height: auto !important; overflow: visible !important; }
    body > * { display: none !important; }

    /* Dynamically target whichever container is active for printing */
    .active-print-container, .active-print-container * { visibility: visible; }
    .active-print-container { display: block !important; position: absolute; left: 0; top: 0; width: 100%; max-width: 800px; margin: 0 auto; box-sizing: border-box; }
    
    /* Subscription Agreement Print Styles */
    #print-container { font-family: 'Times New Roman', serif; padding: 20px; color: black; }
    .print-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid black; padding-bottom: 10px; }
    .print-title { font-size: 22px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; }
    .print-sub { font-size: 11px; font-family: monospace; letter-spacing: 1px; margin-top: 4px; }
    .print-section-title { font-size: 14px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; border-bottom: 1px solid #999; padding-bottom: 4px; margin-top: 24px; margin-bottom: 10px; font-family: monospace; }
    .print-deck-title { font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; margin-top: 14px; margin-bottom: 6px; font-family: monospace; color: #333; }
    .print-deck-body { font-size: 11px; line-height: 1.5; text-align: justify; margin-bottom: 10px; }
    .print-deck-body p { margin: 4px 0; }
    .print-deck-body strong { font-weight: bold; }
    .print-deck-body div { margin: 3px 0; }
    .print-legal { font-size: 12px; line-height: 1.5; margin-bottom: 16px; text-align: justify; white-space: pre-wrap; margin-top: 10px; }
    .print-sigs { display: flex; justify-content: space-between; margin-top: 30px; }
    .print-sig-block { width: 45%; }
    .print-sig-block .sig-name { font-family: cursive; font-style: italic; font-size: 20px; color: #000080; margin-bottom: 4px; padding-bottom: 4px; }
    .print-sig-line { border-top: 1px solid black; margin-top: 2px; padding-top: 8px; }
    .print-sig-block p { margin: 2px 0; font-size: 10px; font-family: monospace; }
    .print-page-break { page-break-before: always; }

    /* Spanish courtesy-translation section — unsigned, watermarked. The watermark is a real text
       overlay (not a CSS background) because browsers drop background-image in print unless the
       user enables "Background graphics"; text/border color prints by default. */
    .print-es-stamp { border: 2px solid #b00000; color: #b00000; font-family: monospace; font-weight: bold; font-size: 12px; letter-spacing: 2px; text-align: center; padding: 6px; margin: 12px 0; text-transform: uppercase; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
    .print-es-note { font-size: 10px; line-height: 1.5; font-style: italic; color: #333; margin-bottom: 14px; text-align: justify; }
    .print-es-body { position: relative; overflow: hidden; }
    .print-es-content { position: relative; z-index: 1; }
    .print-es-content .print-deck-title { color: #000; }
    .print-es-watermark { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: space-around; align-items: center; z-index: 0; overflow: hidden; pointer-events: none; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
    .print-es-watermark > div { transform: rotate(-28deg); white-space: nowrap; font-family: monospace; font-weight: bold; font-size: 21px; letter-spacing: 4px; color: rgba(176,0,0,0.13); }
    .print-es-unsigned { margin-top: 22px; padding: 10px; border: 1px dashed #999; font-family: monospace; font-size: 11px; text-align: center; color: #555; }

    /* Master Roster Print Styles */
    #print-roster-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; padding: 20px 0; max-width: 100%; }
    #print-roster-container .print-header { text-align: left; border-bottom: 1px solid #d1d5db; display: flex; justify-content: space-between; align-items: flex-end; }
    #print-roster-container .print-title { font-size: 18px; letter-spacing: 1px; color: black; }
    #print-roster-container table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    #print-roster-container th { text-align: left; padding: 8px; font-size: 10px; text-transform: uppercase; border-bottom: 1px solid black; }
    #print-roster-container td { padding: 8px; font-size: 11px; border-bottom: 1px solid #e5e7eb; }
    #print-activity-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; padding: 20px 0; max-width: 100%; }
    #print-activity-container .print-header { text-align: left; border-bottom: 1px solid #d1d5db; display: flex; justify-content: space-between; align-items: flex-end; }
    #print-activity-container .print-title { font-size: 18px; letter-spacing: 1px; color: black; }
    #print-activity-container table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    #print-activity-container th { text-align: left; padding: 8px; font-size: 10px; text-transform: uppercase; border-bottom: 1px solid black; }
    #print-activity-container td { padding: 8px; font-size: 11px; border-bottom: 1px solid #e5e7eb; }
    
}


/* Tracker header TABLES/CHARTS toggle — one button, label follows the view. Two body classes drive
   it, both already maintained: charts-on by _setTrackerState, dash-on by navigate().
   VIEW CHARTS shows in exactly ONE case — you are on the tracker AND looking at the tables. Every
   other case offers VIEW TABLES, including "not on the tracker at all", which is why the bare
   default below is TABLES: at boot the portal opens on VISION with neither class set, and the first
   press has to land on the tables rather than skip them. */
#chart-btn-toggle .lbl-charts { display: none; }
#chart-btn-toggle .lbl-tables { display: inline; }
body.dash-on:not(.charts-on) #chart-btn-toggle .lbl-charts { display: inline; }
body.dash-on:not(.charts-on) #chart-btn-toggle .lbl-tables { display: none; }

/* ══ ENGINE STYLES (imported 2026-08-25) ═══════════════════════════════════════════════════════
   The 46 rules from engine/styles.css that have no portal counterpart and cannot match portal
   markup. NOTE 2026-08-25: that verification tested CLASS and ID tokens only, so bare element
   selectors (input, select, select option, input:focus...) were waved through as 'cannot match' when
   they match everything. They overrode .dj-clock with !important and turned every console clock back
   into a black box. Removed at the time; the portal had no input/select styling of its own, so that
   left every engine field white. Restored 2026-08-25 at the foot of this file, scoped away from the
   console. Any future import must test element selectors too, not just class and id tokens.
   Verified against every class and id in public/index.html, so nothing here changes how the
   portal renders today — they sit inert until the engine's markup arrives.
   NOT imported: 12 rules whose selectors the portal already uses (.flip-container, .flipper,
   .custom-scrollbar, .nav-btn, .btn-action-uniform, .finesse-table, .status-dot-pulse, #app-header).
   Those are the ones we already ruled on — portal wins — so the engine's versions are dropped, not
   merged. tokens.css was dropped whole: a strict subset, and its dark greens/ambers lose to the
   portal's muted pair on purpose.                                                              */
.ghost-module { opacity: 0 !important; pointer-events: none !important; visibility: hidden !important; transition: all 0.2s ease; }
.ghost-tab { opacity: 0.35 !important; pointer-events: none !important; }
.remove-module { display: none !important; }
body.god-mode-active .ghost-module, body.god-mode-active .remove-module { display: block !important; opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; border: 2px dashed red !important; position: relative; z-index: 50; }
.flip-indicator { position: absolute; bottom: 10px; right: 10px; width: 14px; height: 14px; color: var(--foundation-blue); opacity: 0.15; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
.btn-mint { background-color: #16a34a; color: white; border: none; }
.btn-mint:hover { filter: brightness(1.1); }
.btn-unmint { background-color: var(--foundation-blue); color: white; border: none; }
.btn-unmint:hover { filter: brightness(1.1); }
.btn-transfer { background-color: #eab308; color: white; border: none; }
.btn-transfer:hover { filter: brightness(1.1); }
.btn-cancel { background-color: #dc2626; color: white; border: none; }
.btn-cancel:hover { filter: brightness(1.1); }
.btn-amber { background-color: var(--status-amber); color: white; border: none; }
.btn-amber:hover { filter: brightness(1.1); }
.btn-green { background-color: var(--status-green); color: white; border: none; }
.btn-green:hover { filter: brightness(1.1); }
.gov-col { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }
.gov-pip { width: 9px; height: 9px; border-radius: 50%; background: var(--border-silver); transition: all 0.3s; opacity: 0.4; }
.gov-pip.on { opacity: 1; background: var(--status-green); }
.gov-pip.pulse-amber { opacity: 1; background: var(--status-amber); animation: pulse 1.5s infinite; }
.gov-pip.pulse-blue { opacity: 1; background: var(--foundation-blue); animation: pulse 1.5s infinite; }
.gov-pip.pulse-green { opacity: 1; background: var(--status-green); animation: pulse 1.5s infinite; }
.spinner-wrapper { position: relative; display: flex; align-items: center; width: 100%; }
.spinner-controls { position: absolute; right: 0.25rem; display: flex; flex-direction: column; justify-content: center; height: 80%; border-left: 1px solid var(--border-silver); padding-left: 4px; }
.spinner-btn { background: transparent; border: none; cursor: pointer; font-size: 11px; color: var(--text-muted); padding: 2px 8px; }
.spinner-btn:hover { color: var(--text-main); }
/* .ghost-row removed 2026-09-03 with the ghost rows themselves (backlog 15/16). Every table that
   padded itself with rows of dashes now renders a real colspan empty state instead. */
/* The three rules below were NEVER IMPORTED from the engine, which is why the freeze toggle looked
   broken: .toggle-slider and its :before existed, but nothing rendered the CHECKED state. The
   checkbox flipped, state.global.isFrozen flipped and the LOCKED/UNLOCKED label flipped — the slider
   stayed a red bar with the knob parked at left:2px, so the control read as dead. Not a JS bug.
   Travel is 20px, not arbitrary: the slider's content box is 44 - 4 (2px border each side) = 40, the
   knob is 16 wide sitting at left:2, so the right rest position is 40 - 16 - 2 = 22, i.e. +20.
   CHECKED = NOT frozen = UNLOCKED (render.js sets `checked = !isFrozen`), so checked is the green
   state and matches the label beside it. */
.hardware-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.hardware-toggle input:checked + .toggle-slider { background-color: var(--status-green); border-color: var(--status-green); }
.hardware-toggle input:checked + .toggle-slider:before { transform: translateX(20px); }
.hardware-toggle { position: relative; display: inline-block; width: 44px; height: 24px; }
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--status-red); transition: .3s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 2px; border: 2px solid var(--status-red); }
.toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .3s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.kpi-row { display: grid; gap: 1rem; align-items: start; grid-template-columns: 1fr; }
/* ONE LINE, ALWAYS. #mod-kpi-row is declared lg:h-[77px] and the four cards inside hold exactly three
   lines of text; a single label taking a second line makes the card 92 and, because align-items above
   is `start` rather than `stretch`, that 15px OVERFLOWS the row rather than being clipped by it. It
   lands in the 16px gap below, so the row reads as having lost its spacing while every card still
   measures its declared size. That is what "Lifetime Flow I/O ($)" did at zoom-out (h 19 -> 37.3), and
   one wrapped label moves all four cards, because grid items share a track height.
   Shortening that string fixed the instance; this fixes the class. Font metrics drift with zoom and
   Safari rounds them up, so any label here is one rendering change from doing it again. The VALUES in
   these cards already carry whitespace-nowrap in the markup and are exactly why none of them ever
   moved — this extends the same guarantee to the labels.
   The ellipsis is not decoration: two of these four fronts have no overflow-hidden of their own, so
   without it a long label would spill sideways out of the card instead of wrapping. */
#mod-kpi-row .front > *,
#mod-kpi-row .back > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* .header-hidden — REMOVED on import, same call as 37d6eab (engine CSS reaching portal elements).
   engine.js's "Smart Header Auto-Hide" listener does getElementById('app-header') on every scroll and
   adds this class past 60px. The engine has its own #app-header; the portal has one too, and loading
   the engine scripts (74c31ba) pointed that listener at OURS — so translateY(-100%) slid the portal
   title off-screen on scroll-down. It read as ordinary scrolling until #nav-shell's sticky was fixed,
   because before that the whole page scrolled away together.
   The listener still fires and still toggles the class; with no rule behind it that is a no-op.
   engine/styles.css:398 keeps its copy, so the engine build is unaffected and a re-sync from engine/
   will not fight this. If the auto-hide is ever wanted for the engine views, bring the rule back
   scoped to body.engine-view — the same containment #orientation-lock uses — never bare. */
/* ORDER IS LOAD-BEARING. Both queries match at >=1280 and both are one class deep, so source
   order alone decides. With 1280 written first (how the portal had it) repeat(2,1fr) won at every
   width and the 3fr/2fr rule never applied anywhere — the KPI row split 50/50 while every card
   below it split 60/40, so the seam between NAV/UNIT and TOTAL UNITS did not land under the seam
   between TREASURY and INFLOW. engine/styles.css:346-354 has them in this order. Narrow query
   first, wide query last. Do not reorder. */
@media (min-width: 768px) {
  .kpi-row { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1280px) {
  /* Must be the SAME track pair as the two card rows below (index.html: the treasury/inflow and
     ownership/valuation rows), or the 16px gutter stops running as one straight vertical line down
     the column. Those rows moved from 3fr/2fr to 1fr/304px when the right-hand cards were fixed and
     the left ones made to flex; this had to move with them. At a 720 column: 720 − 16 = 704, so the
     seam sits at 400 in all three rows instead of 400 / 422 / 400. */
  .kpi-row { grid-template-columns: 376px minmax(0, 1fr); }
}
@media screen and (max-width: 768px) and (orientation: portrait) {
}

/* Rotate-to-view. The cap table does not fit a portrait phone, so LEDGER says "turn your device".
   Keyed to body.needs-landscape (set by navigate() from LANDSCAPE_VIEWS) rather than firing on any
   portrait phone, because every other view is portrait-first. In the engine this was unconditional —
   every view there was a wide one. */
#orientation-lock { display: none; position: fixed; inset: 0; z-index: 99999; background-color: var(--bg-obsidian); flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 2rem; }
.phone-icon-anim { animation: rotatePhone 2s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate; }
@media screen and (max-width: 768px) and (orientation: portrait) {
  body.needs-landscape #orientation-lock { display: flex; }
}
@keyframes rotatePhone {
    0% { transform: rotate(0deg); }
    50% { transform: rotate(-90deg); }
    100% { transform: rotate(-90deg); }
}
@keyframes pulse {
    0% { transform: scale(0.95); opacity: 0.7; }
    50% { transform: scale(1.1); opacity: 1; }
    100% { transform: scale(0.95); opacity: 0.7; }
}

/* ══ ENGINE FORM CONTROLS (restored 2026-08-25) ════════════════════════════════════════════════
   engine/styles.css:208 styled inputs with bare element selectors. Imported raw they hit every
   console clock and turned it into a black box, so they were deleted outright — which is why the
   Valuation Tool, all 11 omni-searches, and every field in Operations and Members render white:
   with no rule, they fall back to a white default instead of --terminal-recess (#0a0c10 in dark).

   Deleting was the wrong fix; scoping is the right one. Two exclusions, both measured:
     [class*="dj-"]  — 286 of the 386 form controls in this file are console. Every one of them
                       carries a dj- prefixed class, so this leaves the console exactly as it was.
     checkbox/radio  — appearance:none blanks a native checkbox. #action-confirm-check is visible
                       (14px, line 457); the engine never had a visible bare checkbox, the portal
                       does, so the rule must not reach it.

   No !important anywhere: on Tailwind v4 every utility sits in @layer utilities and unlayered app
   CSS outranks it regardless of specificity. That is the whole reason for the v4 move — the rule
   wins on its own, and nothing here has to race.                                                */
input:not([type="checkbox"]):not([type="radio"]):not([class*="dj-"]),
select:not([class*="dj-"]),
.pseudo-input {
    background: var(--terminal-recess);
    border: 1px solid var(--border-silver);
    border-radius: 2px;
    color: var(--text-main);
    font-family: 'JetBrains Mono', monospace;
    appearance: none;
}

input:not([type="checkbox"]):not([type="radio"]):not([class*="dj-"]):focus,
select:not([class*="dj-"]):focus {
    border-color: var(--foundation-blue);
    outline: none;
}

/* Lets `border-none` actually mean something on an input or select. The blanket rule above is
   UNLAYERED and Tailwind's utilities are in @layer utilities, so `border-none` in the markup lost
   silently — the v4 cascade trap. Two places wore it without effect:
     #inv-tx-ref-input  — inside a .pseudo-input wrapper that already draws the border, so the input
                          drew a SECOND one inside it. Shorter than the 32px row, so its top and
                          bottom edges read as stray lines above and below the AUTO placeholder.
     #lifecycle-selector — same, minus a wrapper.
   Scoped to elements that explicitly ask for it, so every other input keeps the shared border.

   The :not chain is repeated verbatim, and that is the whole point: the blanket rule scores (0,3,1),
   so `.pseudo-input > input` (0,1,1) and `input.border-none` (0,1,1) both LOSE to it. Both rules are
   unlayered, so source order is never consulted — specificity decides and the shorter selector never
   applied. Carrying the chain puts these at (0,4,1). Do not "simplify" them back. */
input.border-none:not([type="checkbox"]):not([type="radio"]):not([class*="dj-"]),
select.border-none:not([class*="dj-"]) {
    border: none;
}
/* An input inside a .pseudo-input is a text slot in someone else's box: the wrapper owns the border
   and the fill, so the input must not paint either. */
.pseudo-input > input:not([type="checkbox"]):not([type="radio"]):not([class*="dj-"]) {
    border: none;
    background: transparent;
}

/* --terminal-recess already flips with .dark-mode, so the engine's separate .dark-mode input rule
   was a no-op restating the same value. Dropped rather than carried over. */

input:not([class*="dj-"])::placeholder,
textarea:not([class*="dj-"])::placeholder {
    color: var(--text-muted);
    opacity: 1;
}

select:not([class*="dj-"]) option {
    background-color: var(--card-graphite);
    color: var(--text-main);
}

/* ── DISPLAY UTILITIES, UNLAYERED ────────────────────────────────────────────────────────────────
   Counterpart to the note at line 1240. That rule — unlayered app CSS outranks @layer utilities —
   is what the v4 move was FOR, but it cuts both ways: it also means Tailwind can no longer hide
   anything this file gives a display to. Under v3 tailwind.css was unlayered and loaded last
   (index.html:15, after this file at :11), so .lg\:hidden beat .nav-btn on source order. On v4 it
   cannot beat it at all, at any specificity.

   What that broke: the nav slot system (§16oo-B) marks a mobile-only slot `lg:hidden` and a
   desktop-only one `hidden lg:inline-flex`. Against `.nav-btn { display:inline-flex }` (line 272)
   neither lands, so every slot rendered at every breakpoint — DEADLINES and APPLY (dt:{show:false})
   sat in the desktop nav, desktop slots sat in the mobile nav, and both `.nav-page-strip` chip
   strips (line 861) painted at once. 73 other display declarations in this file are the same trap.

   Re-declaring the display utilities here, unlayered and last, restores v3 source-order behaviour
   for display only — the rest of the file keeps winning as v4 intends. NOT !important on .nav-btn:
   important would strengthen the rule that is already winning. Breakpoints match line 844 and
   Tailwind's lg (64rem) / sm (40rem).

   The list is exactly what the markup uses today, counted over index.html + public/*.js:
   lg:hidden 19, lg:inline 10, lg:grid 9, lg:flex 6, sm:grid 4, lg:block 2, lg:inline-flex 1.
   Add a variant here when you first use it — an unlisted one is inert, the same silent-nothing
   failure §16oo-H describes for classes outside the content glob.                                */
.hidden { display: none; }
@media (min-width:640px) {
    .sm\:grid        { display: grid; }
}
@media (min-width:1024px) {
    .lg\:hidden      { display: none; }
    .lg\:block       { display: block; }
    .lg\:inline      { display: inline; }
    .lg\:flex        { display: flex; }
    .lg\:inline-flex { display: inline-flex; }
    .lg\:grid        { display: grid; }
}

/* ── RING-CLOSE PING ──────────────────────────────────────────────────────────────────────────
   Fires on a live roster change: the status dot sweeps a ring around itself 0->360, then the ring
   bursts outward and fades while the dot flashes, and it settles back into its normal breathe.
   Apple-Watch-ring-close, scaled to a 7px dot.
   Driven by data-ping on each dot ("funded" | "intent") so a new funding only lights the green dots
   and a new intent only the amber/purple ones — pingDots() in index.html adds .dot-ping and strips
   it when the animation ends.
   currentColor throughout: every dot already sets `text-[var(--status-*)]` to match its own metric,
   so the ring inherits the right colour with no per-dot rules.
   @property is what makes the conic sweep interpolate — an unregistered custom property jumps from
   0 to 360 with no intermediate frames. Safari 16.4+/Chrome 85+. Where it is unsupported the sweep
   is skipped and only the burst plays, which still reads as "something landed". */
@property --ping-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }

.dot-ping { position: relative; }
.dot-ping::before,
.dot-ping::after { content: ''; position: absolute; border-radius: 50%; pointer-events: none; }

/* the sweep: a conic wedge masked to a 2px ring sitting just outside the dot */
.dot-ping::before {
    inset: -4px;
    background: conic-gradient(from -90deg, currentColor var(--ping-angle), transparent 0);
    -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
            mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
    animation: ping-sweep .55s linear forwards;
}
/* the burst: a ring that expands out of the dot and fades, once the sweep has closed */
.dot-ping::after {
    inset: -4px;
    border: 2px solid currentColor;
    opacity: 0;
    animation: ping-burst .65s cubic-bezier(.2,.7,.3,1) .55s forwards;
}
/* the dot itself flashes at the moment of close, then hands back to `breathe` */
.dot-ping { animation: ping-flash 1.2s ease-out 1; }

@keyframes ping-sweep { from { --ping-angle: 0deg; } to { --ping-angle: 360deg; } }
@keyframes ping-burst {
    0%   { opacity: 1; transform: scale(1);   }
    100% { opacity: 0; transform: scale(2.6); }
}
@keyframes ping-flash {
    0%   { opacity: 1; box-shadow: 0 0 8px currentColor;  }
    46%  { opacity: 1; box-shadow: 0 0 8px currentColor;  }
    55%  { opacity: 1; box-shadow: 0 0 16px currentColor, 0 0 26px currentColor; }
    100% { opacity: 1; box-shadow: 0 0 8px currentColor;  }
}
@media (prefers-reduced-motion: reduce) {
    .dot-ping, .dot-ping::before, .dot-ping::after { animation: none !important; }
}

/* ── FORCE-STACK — double-tap the masthead to hold the stacked view on any width ──────────────
   Normally the one/two-column split is `wide:` (1900px). This pins it to one column regardless,
   so a wide monitor can run the stacked terminal layout on purpose. Persisted as ahwf-stack and
   re-applied in the <head> boot script beside ahwf-theme / ahwf-navstyle, so it survives a reload
   without a flash of two-column.

   No !important, and none is needed. These are UNLAYERED, and unlayered CSS outranks anything in
   @layer utilities at any specificity — the v4 behaviour this file's migration was built on
   (see the note at :1672). So they beat wide:grid-cols, wide:sticky and the row-gap utility on
   layer alone; the id selectors are just precision, not force.

   THE 1024 GUARD IS LOAD-BEARING: below it the portal is the mobile layout, which is already one
   column and whose row gap is a deliberate 16px. Without this media query, forcing the flag on and
   then narrowing to a phone would inject a 192px gap into mobile. Mobile must not see these rules.

   row-gap repeats the 192 from index.html's lg:max-wide:gap-y-[192px] because that utility stops
   at 1900 by design and this state lives past it. Two places, one number — change both. */
@media (min-width:1024px) {
    body.force-stack #app-main      { grid-template-columns: 1fr; row-gap: 192px; }
    body.force-stack #nav-band      { grid-template-columns: 1fr; }
    body.force-stack #charts-column { position: static; }

    /* 1200 content cap, chosen 2026-09-01. Note 1195 is the ledger native width, so at cap
       table was built for (index.html:3423, derived at :2754 as 505 + 642 + 16 gap + 32 padding).
       1200 the cap table renders 27px under it. Past the cap the layout only stretches:
       1900 is TWO-COLUMN arithmetic (32 + 664 + 16 + 1188) and means nothing once there is one
       column. Capping here makes the stacked view render identically on a 13" and a 34" — mx-auto
       centres the leftover, so the monitor stops mattering.

       ALL THREE MOVE TOGETHER. #hdr-row, #nav-band and #app-main share one alignment (see the note
       at index.html:1993); cap one and the other two hang 564px wider than it. The matching
       lg:max-wide:max-w-[1200px] utilities cover 1024-1899; these cover force-stack past 1900. */
    body.force-stack #hdr-row,
    body.force-stack #nav-band,
    body.force-stack #app-main      { max-width: 1132px; }
}




/* RETURN chevron — the same build the FOUNDING INTENT arrow does (#intent-dir-arrow, driven by the
   500ms interval at index.html:7577): > then >> then >>> then blank, on a 4-tick 2s loop. Done in
   CSS rather than by extending that interval, because the interval also recolours itself from fSlot
   and this arrow should not follow the slot selection. steps(1) so each glyph snaps on, no fade —
   the original sets opacity 0/1 outright. */
/* MARCHING CHEVRONS — named for DIRECTION, not for the button that happens to use them.
   They were .ret-chev / .exp-chev, named for purpose, and the day the direction convention flipped
   those names meant the opposite of what they said. Direction plus march order is all they encode.

   BACK IS LEFT. Every return action in the app points left: browser back, OS back, breadcrumbs — it
   is the strongest directional convention on the web, and here the tracker being returned to also
   physically sits on the left, so the arrow points at the thing it restores. The earlier build read
   the arrow as which way the PANEL travels, which is internally consistent and still wrong: nobody
   parses a return as "the content edge will translate rightward."

   Delays make them march in the direction they point — .chev-right lights left-to-right, .chev-left
   right-to-left — off the one retChev keyframe. */
.chev-right i, .chev-left i { font-style: normal; display: inline-block; opacity: 0; animation: retChev 2s linear infinite; }
.chev-right i:nth-child(1) { animation-delay: 0s; }
.chev-right i:nth-child(2) { animation-delay: .5s; }
.chev-right i:nth-child(3) { animation-delay: 1s; }
.chev-left  i:nth-child(1) { animation-delay: 1s; }
.chev-left  i:nth-child(2) { animation-delay: .5s; }
.chev-left  i:nth-child(3) { animation-delay: 0s; }

/* The LETTER layout keeps a 768px reading measure (max-w-3xl), inherited from the apex shell it
   replaced. In the normal ~558px pane it never binds. Expanded to 1079 it does, so the letter was the
   one section that did not fill the space it had just been given — it centred and left gutters.
   Released while expanded: asking for full view and getting a centred column is the wrong answer to
   the button you pressed. The cap still applies at normal width, where it costs nothing. */
body.pillar-collapsed .letter-measure { max-width: none; }

.sec-return { display: none; }
body.pillar-collapsed .sec-expand { display: none; }
body.pillar-collapsed .sec-return { display: inline-flex; }
/* linear, not steps(1): steps(1) quantises the WHOLE animation to one step, so it held the 0%
   value for the entire cycle and nothing ever appeared. The hard 24.99/25 pair snaps instead. */
@keyframes retChev { 0%, 24.99% { opacity: 0; } 25%, 100% { opacity: 1; } }

/* BADGE TILES — member card, back of #mod-member-tier2. Two states and the DEFAULT IS UNEARNED, so a
   tile that is never wired reads as not-yet rather than as a claim. .earned is what lights it, and
   nothing sets that class today (backlog 41/42). Colour arrives per tile as --bc so one rule serves
   all four. */
.badge-tile { border-color: var(--border-silver); background: transparent; opacity: .45; transition: opacity .2s, border-color .2s, background .2s; }
.badge-tile .badge-tile-name { color: var(--text-muted); }
.badge-tile .badge-tile-kind { color: var(--text-muted); opacity: .7; }
.badge-tile.earned .badge-tile-kind { color: var(--bc); opacity: .8; }
.badge-tile.earned { opacity: 1; border-color: color-mix(in srgb, var(--bc) 33%, transparent); background: color-mix(in srgb, var(--bc) 9%, transparent); }
.badge-tile.earned .badge-tile-name { color: var(--bc); }

/* ── THE TRACK IS THE DIVIDER ───────────────────────────────────────────────────────────────────
   A host that draws its own border-bottom UNDER a .scroll-progress-track renders both: 2px of
   --border-silver plus a 1px border, and it reads visibly heavier. That is the whole difference
   between the console's directive heads — no border, a clean 2px, the look we want everywhere — and
   the eight hosts that carried one: funded / intent / sec, the roster / access-requests /
   portal-activity console tables, the unit lockup head, and the board's POSITION METRICS bar.

   Written as :has(>) rather than eight edits so a host can never disagree with its own track again —
   add a track anywhere and the border steps aside by construction. Placed at the end of the file on
   purpose: .tracker-head declares border-bottom at :1428 and both selectors are (0,1,0), so source
   order is what settles it. Tailwind's border-b utilities are layered and lose to any unlayered rule
   regardless of position. */
:has(> .scroll-progress-track) { border-bottom: none; }
