/* ============================================================================
   APPUI — the "this is an application, not a web page" layer for the APP FLEET.

   Loaded ONLY by the admin/portal shells, AFTER app.css:
     app/Views/layouts/app.php · portal.php · bare.php · docs.php

   ── WHY A SEPARATE SHEET (READ BEFORE EDITING) ────────────────────────────
   app.css is NOT shared across the fleet. Measured 2026-09-18 over 112 app
   directories: 29 carry the skeleton's byte-identical canonical block, 13 are
   base+appended, and 70 have an EDITED base (older skeleton generations, plus
   the de-branding sweeps that renamed classes inside it — taskora's `.skeleton`
   became `.employee-task-reports`). So there is no version of "overwrite
   app.css everywhere" that is safe.

   This file sidesteps all of that. It only OVERRIDES classes the canonical
   block already defines, by cascade order, so:
     · every app inherits the new look with ZERO view edits;
     · a diverged app.css is irrelevant — we never touch it;
     · the blast radius is exactly the shells that link it;
     · it reverts by deleting one <link>, with nothing to untangle.

   ── THE RULE FOR EDITING THIS FILE ────────────────────────────────────────
   Never rename or redefine a class app.css owns — OVERRIDE it. Views across
   112 apps use these names and we cannot grep them all with confidence.
   Everything new is namespaced `.ap-*` and is strictly opt-in.

   ── SCOPING ───────────────────────────────────────────────────────────────
   Scoped to `body.admin` / `body.public`, which every app layout already sets.
   That is the equivalent of the platform's `body.has-shell` — and it means the
   marketing landing (`ld-*`, no body class) is deliberately untouched.

   ⚠️ DO NOT ADD `overscroll-behavior-y: none`. app.css sets
   `body{overflow-x:hidden}`, which makes BODY a scroll container whose content
   exactly fits while <html> is what actually scrolls. Forbidding the gesture
   from chaining body -> html freezes the page for wheel and touch, while
   scrollHeight checks and window.scrollTo() both still pass. (Cost the platform
   a day on 2026-09-12; same body rule exists here.)
   ============================================================================ */

body.admin, body.public {
    /* ── Motion. One spring, one decel curve. An app responds; it does not
       animate at you. Anything over ~280ms reads as "the page is loading". */
    --e-out:    cubic-bezier(.32, .72, 0, 1);
    --e-spring: cubic-bezier(.34, 1.36, .64, 1);
    --t-fast: .14s;
    --t-base: .22s;

    /* ── Native-app radii. Phone UI is rounder than web UI; this single step up
       (10/14/18 -> 12/16/20) is most of the difference between "a page of
       boxes" and "an app screen". Overrides app.css's :root values. */
    --radius-sm: 12px;
    --radius:    16px;
    --radius-lg: 20px;
    --r-xl:      24px;

    /* ── Elevation. Tinted, not grey — a neutral drop shadow on a warm surface
       reads as dirt. These are the only shadows this layer uses. */
    --el-1: 0 1px 2px rgba(16,24,40,.05), 0 1px 3px rgba(16,24,40,.04);
    --el-2: 0 2px 4px -1px rgba(16,24,40,.06), 0 6px 16px -6px rgba(16,24,40,.10);
    --el-3: 0 8px 24px -8px rgba(16,24,40,.16), 0 2px 6px -2px rgba(16,24,40,.06);
    --shadow-sm: var(--el-1);
    --shadow:    var(--el-2);
    --shadow-lg: var(--el-3);

    /* ── Functional tints, DELIBERATELY INDEPENDENT OF THE BRAND COLOUR.
       Every app is white-label: --sky/--blue is whatever palette the buyer
       picked, so tinting a dozen metric icons with it produces a dozen squares
       of the same colour. These carry MEANING instead (money is emerald, danger
       is rose) and stay constant, so an icon's colour becomes a landmark.
       The brand colour still owns every primary affordance: buttons, active
       nav, focus rings, links. Contrast-checked against #fff and --bg-2. */
    --t-indigo:  #4338ca; --tb-indigo:  #eef2ff;
    --t-violet:  #6d28d9; --tb-violet:  #f3e8ff;
    --t-rose:    #be123c; --tb-rose:    #ffe4e6;
    --t-orange:  #c2410c; --tb-orange:  #ffedd5;
    --t-amber:   #92400e; --tb-amber:   #fef3c7;
    --t-emerald: #047857; --tb-emerald: #d1fae5;
    --t-teal:    #0f766e; --tb-teal:    #ccfbf1;
    --t-sky:     #0369a1; --tb-sky:     #e0f2fe;
    --t-slate:   #475569; --tb-slate:   #f1f5f9;

    /* Canvas a hair cooler than app.css's --bg so white cards lift off it. */
    --app-canvas: #f6f7f9;
}

/* ============================================================================
   1 · GLOBAL APP BEHAVIOURS + PRESS FEEDBACK
   A web page reacts on mouseup; an app reacts the instant your finger lands.
   ============================================================================ */
body.admin, body.public { -webkit-tap-highlight-color: transparent; }
body.admin a, body.admin button, body.admin .icon-btn,
body.public a, body.public button, body.public .icon-btn { touch-action: manipulation; }

body.admin .admin-sidebar, body.admin .admin-mobile-bar,
body.public .public-header, body.public .tabbar {
    -webkit-user-select: none; user-select: none;
}

body.admin .card, body.admin .stat, body.admin .kv, body.admin .chip,
body.admin .cat-tile, body.admin .btn, body.admin .icon-btn,
body.public .card, body.public .stat, body.public .chip, body.public .btn {
    transition: transform var(--t-fast) var(--e-out),
                box-shadow var(--t-fast) var(--e-out),
                border-color var(--t-fast), background-color var(--t-fast);
}
body.admin a.card:active, body.admin a.stat:active, body.admin .chip:active,
body.admin .cat-tile:active, body.public a.card:active, body.public .chip:active { transform: scale(.975); }
body.admin .btn:active, body.public .btn:active { transform: scale(.97); }

/* ============================================================================
   2 · NUMERALS — the headline fix.

   THE BUG (measured on billora, 375px, 2026-09-18): app.css sets
   `.stat .value { font-size:28px; overflow-wrap:anywhere }`. In a half-width
   card a real money string has nowhere to go, so `overflow-wrap:anywhere`
   breaks it MID-NUMBER: "Rs 67,626." on one line and "00" on the next. Three
   of six cards on the dashboard were broken that way. It reads as a rendering
   fault, and it gets worse as the customer's business grows.

   THE FIX is three things, none of which changes a displayed value:
     (a) tabular figures, so digits share one advance width and columns line up;
     (b) container-query sizing — the number shrinks to fit ITS OWN card rather
         than the viewport, so a long total degrades gracefully instead of
         wrapping. `cqi` falls back to the clamp floor where unsupported;
     (c) never break inside a number. `overflow-wrap:normal` undoes `anywhere`.
   appui.js then demotes a trailing ".00" to a small muted suffix, which buys
   back ~18% of the width for free. The digits themselves are never rewritten.
   ============================================================================ */
body.admin .stats > *, body.public .stats > * { container-type: inline-size; }

body.admin .stat .value, body.public .stat .value,
body.admin .kv .v, body.admin .stat-value, body.admin .ap-num {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1, "cv11" 1;
    overflow-wrap: normal;
    word-break: normal;
    letter-spacing: -.03em;
}
body.admin .stat .value, body.public .stat .value {
    font-size: clamp(18px, 13cqi, 27px);
    line-height: 1.12;
    margin-top: 5px;
}
/* The decimal suffix appui.js splits off. Same colour family, 62% the size —
   present for anyone auditing a figure, silent for everyone scanning. */
body.admin .stat .value .ap-dec, body.public .stat .value .ap-dec,
body.admin .ap-num .ap-dec {
    font-size: .6em; font-weight: 700; color: var(--muted);
    letter-spacing: 0; margin-left: 1px;
}
body.admin .stat.accent .value .ap-dec { color: rgba(255,255,255,.7); }

/* Money and counts inside tables line up under each other. */
body.admin .table td, body.admin .table th,
body.public .table td, body.public .table th { font-variant-numeric: tabular-nums; }

/* ============================================================================
   3 · STAT / METRIC CARDS
   ============================================================================ */
body.admin .stats, body.public .stats {
    /* minmax(0,1fr) — NOT 1fr. A bare 1fr is minmax(auto,1fr), so a track can
       never shrink below its content and one long figure widens the whole row. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}
@media (min-width: 720px) {
    body.admin .stats, body.public .stats { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
}
body.admin .stat, body.public .stat {
    border-radius: var(--radius);
    padding: 14px 15px;
    box-shadow: var(--el-1);
    min-width: 0;
    position: relative;
    overflow: hidden;
}
body.admin a.stat:hover, body.public a.stat:hover { box-shadow: var(--el-2); border-color: var(--line-2); }
body.admin .stat .label, body.public .stat .label {
    font-size: 12px; font-weight: 600; color: var(--muted);
    min-width: 0; line-height: 1.3;
}
/* A label is a label, not a headline — clip it rather than let two words of
   "Outstanding receivables" push the card's own number out of shape. */
body.admin .stat .label > span, body.public .stat .label > span {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
body.admin .stat .label i, body.public .stat .label i { font-size: 15px; flex: none; opacity: .75; }
body.admin .stat .meta, body.public .stat .meta { font-size: 11.5px; margin-top: 3px; }
body.admin .stat.accent, body.public .stat.accent { box-shadow: 0 10px 24px -12px var(--blue); }

/* ⚠️ THE ACCENT CARD MUST BE RE-ASSERTED, NOT INHERITED.
   app.css says `.stat.accent .label{color:rgba(255,255,255,.85)}` at (0,0,3,0).
   The `body.admin .stat .label` rule above is (0,0,3,1) — one class heavier —
   so it silently won and painted a grey label onto the brand gradient. The
   first screenshot after the change caught it; no assertion would have. Any
   new `.stat .*` rule added above needs its accent twin added here. */
body.admin .stat.accent .label, body.public .stat.accent .label,
body.admin .stat.accent .label i, body.public .stat.accent .label i { color: rgba(255,255,255,.9); opacity: 1; }
body.admin .stat.accent .meta, body.public .stat.accent .meta { color: rgba(255,255,255,.78); }
body.admin .stat.accent .value, body.public .stat.accent .value { color: #fff; }

/* ============================================================================
   4 · CARDS, SURFACES, SECTIONS
   ============================================================================ */
body.admin { background: var(--app-canvas); }
@media (min-width: 1024px) { body.admin { background: var(--app-canvas); } }

body.admin .card, body.public .card {
    border-radius: var(--radius);
    box-shadow: var(--el-1);
    padding: 16px;
}
body.admin .card.lg, body.public .card.lg { border-radius: var(--radius-lg); padding: 22px; }
body.admin .card-title, body.public .card-title {
    font-size: 15px; font-weight: 800; letter-spacing: -.015em; margin-bottom: 12px;
}
body.admin .kv { border-radius: 14px; background: #fff; box-shadow: var(--el-1); border-color: var(--line); }
body.admin .kv .k { font-size: 11.5px; }
body.admin .kv .v { font-size: 15.5px; font-weight: 800; letter-spacing: -.02em; }
body.admin .section-title { font-size: 12px; letter-spacing: .05em; margin: 22px 0 9px; }
body.admin .divider { margin: 14px 0; }

/* ============================================================================
   5 · TABLES — DESKTOP
   The header row stays put while the body scrolls. On a 40-row invoice list
   that is the difference between reading a column and remembering it.
   ============================================================================ */
body.admin .table-wrap, body.public .table-wrap {
    border-radius: var(--radius); box-shadow: var(--el-1);
}
body.admin .table th, body.public .table th {
    font-size: 11px; letter-spacing: .05em; color: var(--muted-2);
    padding: 11px 14px; background: var(--bg-2);
    position: sticky; top: 0; z-index: 2;
}
body.admin .table td, body.public .table td { padding: 12px 14px; }
body.admin .table tr:last-child td, body.public .table tr:last-child td { border-bottom: none; }
body.admin .table tbody tr, body.public .table tbody tr { transition: background-color var(--t-fast); }
body.admin .table tr:hover td, body.public .table tr:hover td { background: var(--bg-2); }
/* `.r` is the canonical right-align class the list views already put on money
   columns — give it the alignment AND the figure treatment in one place. */
body.admin .table .r, body.public .table .r { text-align: right; font-variant-numeric: tabular-nums; }

/* ============================================================================
   6 · TABLES — MOBILE (<=680px). The density fix.

   THE BUG: app.css turns each <td> into its own full-width row, so a 7-column
   invoice list renders seven stacked lines per record, each repeating an
   uppercase label. Measured on billora at 375px: ONE invoice filled 530px —
   1.5 records per screen, and the words DOCUMENT/TYPE/DATE/CUSTOMER/TOTAL/
   PAYMENT repeated once per row for all 40 rows. That is the "text is too
   much" the brief describes, and it is pure chrome.

   THE FIX: the row becomes a wrapping flex container and each cell becomes a
   compact field (quiet label above, value below), so short fields sit side by
   side instead of each claiming a line. The first cell is promoted to a title
   — safe fleet-wide, because the first column identifies the record in every
   list table there is. Labels stay in the DOM for screen readers; they just
   stop shouting.
   ============================================================================ */
@media (max-width: 680px) {
    /* ⚠️ `:has(.table-as-cards)` IS LOAD-BEARING, NOT A FLOURISH.
       Dropping the wrapper's border and clipping is right ONLY when the table
       inside has become cards — the cards are the frame, and `overflow:hidden`
       would crop their shadow. But `.table-wrap` is also the horizontal SCROLL
       CONTAINER (`overflow-x:auto`) for every table that stays a table, and
       plenty do: a view only becomes cards if its author added
       `table-as-cards`.

       The first version of this rule was unscoped, so on those pages the table
       lost its scroller and pushed the whole PAGE sideways — 375px viewport,
       603px document, every row's text squeezed into three lines. It shipped,
       and it was found by measuring `scrollWidth > clientWidth` on a live demo
       rather than by looking at a page that seemed fine.

       A browser without `:has()` keeps the original bordered, scrolling
       wrapper, which is the safe direction to fail in. */
    body.admin .table-wrap:has(.table-as-cards),
    body.public .table-wrap:has(.table-as-cards) {
        border: none; background: transparent; box-shadow: none; overflow: visible;
    }
    body.admin .table-as-cards tr, body.public .table-as-cards tr {
        display: flex; flex-wrap: wrap; align-items: flex-start;
        column-gap: 16px; row-gap: 8px;
        border: 1px solid var(--line); border-radius: var(--radius);
        background: #fff; box-shadow: var(--el-1);
        padding: 12px 14px; margin-bottom: 8px;
    }
    body.admin .table-as-cards td, body.public .table-as-cards td {
        display: flex; flex-direction: column; align-items: flex-start;
        width: auto; flex: 0 1 auto; min-width: 0; max-width: 100%;
        padding: 0; gap: 1px;
        font-size: 13.5px; font-weight: 600; color: var(--ink-2);
    }
    body.admin .table-as-cards td:before, body.public .table-as-cards td:before {
        content: attr(data-label);
        font-size: 10.5px; font-weight: 600; text-transform: uppercase;
        letter-spacing: .04em; color: var(--muted-2); line-height: 1.4;
    }
    /* A cell with no data-label would otherwise reserve an empty label line. */
    body.admin .table-as-cards td:not([data-label]):before,
    body.public .table-as-cards td:not([data-label]):before { display: none; }

    /* The record's identity: full width, no label, its own line. */
    body.admin .table-as-cards td:first-child, body.public .table-as-cards td:first-child {
        flex: 1 0 100%;
        font-size: 15.5px; font-weight: 700; color: var(--ink);
        letter-spacing: -.015em;
    }
    body.admin .table-as-cards td:first-child:before,
    body.public .table-as-cards td:first-child:before { display: none; }
    body.admin .table-as-cards td:first-child strong { font-weight: 700; }

    /* Row actions ride the title line's right edge instead of owning a line. */
    body.admin .table-as-cards td:last-child:not([data-label]),
    body.public .table-as-cards td:last-child:not([data-label]) {
        margin-left: auto; flex-direction: row; gap: 6px; align-self: center;
    }
    body.admin .table-as-cards td:empty, body.public .table-as-cards td:empty { display: none; }

    /* Right-aligned money keeps its emphasis; it just stops being right-aligned
       in a context where there is no column to align to. */
    body.admin .table-as-cards td.r, body.public .table-as-cards td.r {
        text-align: left; font-weight: 800; color: var(--ink); letter-spacing: -.02em;
    }
    body.admin .table-as-cards .badge, body.public .table-as-cards .badge { margin-top: 1px; }
}

/* ============================================================================
   6b · GRID TRACKS MUST BE ABLE TO SHRINK

   `1fr` means `minmax(auto, 1fr)`, and that `auto` floor is the item's
   min-content width. So a card holding a wide form or table sets the track's
   MINIMUM, and a one-column grid inside a 347px container can resolve to a
   407px track and push the page sideways.

   This was the last shared defect in the fleet — four of the final five
   overflows were exactly it, all on `.split`:
     fleet-management /admin/behavior      407px track in a 347px container
     photography-studio /admin/payments    436px
     solar-amc-management /admin/subsidy   407px
     physiotherapy-clinic /admin/referrals 382px
   and the fifth was `.kv-grid` at 210+225px inside 313px.

   ⭐ THE FIX IS ON THE ITEMS, NOT THE TRACKS. Overriding
   `grid-template-columns` would work (verified, 5/5) but would mean restating
   every breakpoint these utilities use — and app.css has diverged across 70 of
   112 apps, so an app that tuned its own breakpoints would silently inherit the
   skeleton's. `min-width:0` on the CHILD zeroes the automatic minimum that the
   `auto` floor is computed from, which fixes the same 5/5 while touching no
   column definition at all and needing no !important.

   It is a no-op wherever the content already fits: `1fr` and a shrinkable `1fr`
   resolve identically until the floor would have been exceeded, which is
   precisely the broken case.
   ============================================================================ */
body.admin .split > *, body.public .split > *,
body.admin .kv-grid > *, body.public .kv-grid > *,
body.admin .grid-3 > *, body.public .grid-3 > *,
body.admin .cat-grid > *, body.public .cat-grid > * { min-width: 0; }

/* A key/value tile's own text still has to be breakable, or the tile just
   overflows the (now correctly sized) track instead. "Committed (PO)" beside
   a long total was 226px in a 210px track. */
body.admin .kv, body.public .kv,
body.admin .kv .k, body.admin .kv .v { overflow-wrap: break-word; }

/* ============================================================================
   7 · ADMIN SHELL
   ============================================================================ */
body.admin .admin-sidebar { padding: 16px 12px; box-shadow: none; }

/* A CLOSED off-canvas sidebar must be hidden, not merely pushed off-screen.
   `translateX(-100%)` lands its right edge exactly on x=0, and on a non-integer
   device pixel ratio the boundary column still paints — a stray sliver of nav
   text was visible at the left edge of every phone screenshot. Hiding it also
   closes a real accessibility defect that predates this layer: the nav links
   were off-screen but still focusable, so keyboard and screen-reader users tab
   through the whole menu before reaching the page.
   `visibility` is delayed on close so the slide-out is still seen, and
   immediate on open. Desktop (>=1024px) is untouched — there the sidebar is
   `transform:none` and permanently visible. */
@media (max-width: 1023px) {
    body.admin .admin-sidebar {
        visibility: hidden;
        transition: transform var(--t-base) var(--e-out), visibility 0s linear var(--t-base);
    }
    body.admin .admin-sidebar.open {
        visibility: visible;
        transition: transform var(--t-base) var(--e-out), visibility 0s;
        box-shadow: var(--el-3);
    }
}
body.admin .admin-brand { padding: 4px 6px 14px; }
body.admin .admin-brand .logo { border-radius: 13px; box-shadow: var(--el-1); }
body.admin .admin-brand h1 { font-size: 14.5px; letter-spacing: -.02em; }
body.admin .admin-brand p {
    font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.admin .admin-nav { gap: 2px; }
body.admin .admin-nav .nav-section { font-size: 10px; padding: 15px 12px 5px; }
body.admin .admin-nav a {
    padding: 9px 12px; border-radius: 11px; font-size: 14px; font-weight: 600;
    gap: 11px; min-width: 0;
}
body.admin .admin-nav a span {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
body.admin .admin-nav a .icon { font-size: 19px; flex: none; }
/* Active item: a filled pill that carries the brand, plus a rail so the eye
   finds it without reading. */
body.admin .admin-nav a.active {
    background: var(--sky-soft); color: var(--blue); font-weight: 700;
    position: relative;
}
body.admin .admin-nav a.active:before {
    content: ""; position: absolute; left: -12px; top: 50%; transform: translateY(-50%);
    width: 3px; height: 20px; border-radius: 0 3px 3px 0; background: var(--blue);
}
body.admin .admin-nav a:active { transform: scale(.985); }
body.admin .admin-user-card { padding-top: 12px; }
body.admin .admin-user-card .name,
body.admin .admin-user-card .role { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Mobile bar + desktop header. A hairline under a bar nothing has scrolled
   past is a web-page seam — appui.js adds .is-scrolled once the page moves. */
body.admin .admin-mobile-bar {
    background: rgba(255,255,255,.82);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    border-bottom: 1px solid transparent;
    transition: border-color var(--t-base), box-shadow var(--t-base);
    padding: 10px 14px;
}
body.admin .admin-mobile-bar.is-scrolled { border-bottom-color: var(--line); box-shadow: var(--el-1); }
body.admin .admin-header h1 { font-size: 21px; letter-spacing: -.025em; }
body.admin .admin-header p { font-size: 13.5px; }

/* THE HEADER OVERLAP: `.admin-header .row.between` is a non-wrapping flex row,
   so on a phone the action buttons wrap INSIDE their own box and ride up over
   the page title. Let the row wrap and give the actions their own line. */
@media (max-width: 720px) {
    /* ⚠️ THE WRAP ENABLER AND THE FULL-WIDTH RULE MUST SHARE A SCOPE.
       The first version paired an UNSCOPED `.page-actions{width:100%}` with a
       `flex-wrap:wrap` scoped to `.admin-header .row.between`. Most apps use
       that wrapper so both applied and all was well — but photography-studio's
       `.admin-header` is ITSELF `display:flex; flex-wrap:nowrap` with no
       `.row.between` inside. There the width rule landed and the wrap enabler
       did not, so a 343px actions block sat beside the title in a row that
       could not wrap: 470px of content in a 375px viewport, on 10 of its 12
       screens. A rule that is only safe because of a second rule must not be
       able to apply without it. */
    body.admin .admin-header { padding: 16px 16px 6px; flex-wrap: wrap; }
    body.admin .admin-header > :first-child { min-width: 0; }
    body.admin .admin-header .row.between { flex-wrap: wrap; align-items: flex-start; gap: 10px; }
    body.admin .admin-header .row.between > :first-child { flex: 1 1 100%; min-width: 0; }
    body.admin .admin-header h1 { font-size: 19px; }
    body.admin .admin-header .page-actions { width: 100%; flex: 1 1 100%; }
    body.admin .page-actions .search { flex: 1 1 100%; }
    body.admin .admin-content { padding: 12px 14px calc(28px + env(safe-area-inset-bottom)); }

    /* A `.row` must wrap rather than run off the side.
       `.row` is `display:flex` with NO wrap, which is right on a desktop and
       wrong on a phone: the alternative to wrapping is overflowing. Measured on
       /admin/site across all six site-builder / online-store demos — a
       `<div class="row">` holding [Save] and [View my site] was 302px of space
       for 151px+ of buttons, so the second one sat at x=410 in a 375px
       viewport. Wrapping only engages when the content does not fit, so a row
       that already fits is untouched.
       ⚠️ `.ap-chips` deliberately still wins here: `.row.wrap.ap-chips` is three
       classes against this rule's two, so a chip strip keeps scrolling on one
       line instead of wrapping. Check that if you ever restructure either. */
    body.admin .row, body.public .row { flex-wrap: wrap; }

    /* A file input carries the browser's own "Choose file / no file chosen"
       widget, whose intrinsic width is ~371px — wider than the phone. As a flex
       item its default `min-width:auto` refuses to shrink below that, and it
       drags its form wider with it, so `max-width:100%` alone is circular: 100%
       of a parent the input itself has already stretched. It has to be allowed
       to shrink first. */
    body.admin form.row, body.public form.row { min-width: 0; }
    body.admin input[type="file"], body.public input[type="file"] { min-width: 0; max-width: 100%; }

    /* `.field-row` must collapse on a narrow screen even when a view has set
       its columns inline.

       app.css already says `@media(max-width:520px){.field-row{grid-template-columns:1fr}}`
       — but AN INLINE STYLE OUTRANKS EVERY MEDIA QUERY, and 9 views across 5
       apps carry one (feedback-survey-platform's analytics filter bar is
       `style="grid-template-columns:2fr 1fr 1fr 1fr auto"`). Its five tracks
       measured 559px inside a 313px card: 639px of document in a 375px
       viewport, the worst single overflow in the fleet. `!important` is the
       only thing that beats an inline declaration — the same remedy the
       platform applied to 37 files on 2026-09-13.

       ⚠️ Measured, not assumed: this still overflowed at 600px, so the
       breakpoint is 720 and not app.css's 520 — a `max-width:520px` rule would
       have fixed the phone and left every tablet broken.

       The track is `minmax(min(100%,240px),1fr)` rather than a flat `1fr` so
       that a field row WITHOUT an inline override keeps exactly the layout
       app.css gives it: 1 column at 375px, 2 at 600px. The rule fixes the
       defect without restyling everyone else's forms. */
    body.admin .field-row, body.public .field-row {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)) !important;
    }
}
@media (min-width: 1024px) {
    body.admin .admin-sidebar { border-right: 1px solid var(--line); }
    body.admin .admin-header { padding: 24px 30px 8px; }
    body.admin .admin-content { padding: 12px 30px 44px; }
}

/* ============================================================================
   8 · CONTROLS — buttons, inputs, chips, tabs, pagination
   ============================================================================ */
body.admin .btn, body.public .btn { border-radius: 12px; font-weight: 650; }
body.admin .btn-primary, body.public .btn-primary { box-shadow: 0 6px 14px -8px var(--blue); }
body.admin .btn-secondary, body.public .btn-secondary { box-shadow: var(--el-1); }
body.admin .icon-btn, body.public .icon-btn { border-radius: 11px; box-shadow: var(--el-1); }
body.admin .icon-btn:active, body.public .icon-btn:active { transform: scale(.94); }

body.admin input, body.admin select, body.admin textarea,
body.public input, body.public select, body.public textarea {
    border-radius: 12px; padding: 11px 13px;
    /* 16px is the iOS zoom threshold — anything under it makes Safari zoom the
       page on focus, which then leaves the user zoomed in on a form. */
    font-size: 16px;
}
@media (min-width: 1024px) {
    body.admin input, body.admin select, body.admin textarea { font-size: 14.5px; }
}
body.admin .form-group label { font-size: 12.5px; color: var(--ink-2); }

body.admin .badge, body.public .badge { font-size: 11.5px; padding: 3px 9px; font-weight: 700; }
body.admin .chip, body.public .chip { border-radius: 999px; box-shadow: var(--el-1); }
body.admin .tabs a, body.public .tabs a { font-size: 13.5px; padding: 9px 13px; }

/* Tab strips scroll instead of wrapping.
   `.tabs` (canonical) already had `overflow-x:auto`; this only adds the
   momentum, snap and hidden scrollbar that make it feel like a tab strip
   rather than a box with a scrollbar in it.
   `.subtabs` is app-specific CSS in 7 apps and ships `flex-wrap:wrap`, so a
   long sub-navigation wraps to two or three rows on a phone and pushes the
   content it belongs to off the screen. Same treatment, ZERO markup change —
   this file loads after each app's own app.css, which is the whole point of
   the layer.
   ⚠️ NOT `.pill-row`. That one looks identical in the markup but holds
   `<span class="pill">` STATUS BADGES, not links — "Pending · 80G eligible ·
   Receipt #123". You read all of those at once; scrolling two of them out of
   sight would hide state, not save space. */
body.admin .tabs, body.public .tabs,
body.admin .subtabs, body.public .subtabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
body.admin .tabs::-webkit-scrollbar, body.public .tabs::-webkit-scrollbar,
body.admin .subtabs::-webkit-scrollbar, body.public .subtabs::-webkit-scrollbar { display: none; }
body.admin .tabs > *, body.public .tabs > *,
body.admin .subtabs > *, body.public .subtabs > * {
    flex: none; scroll-snap-align: start; white-space: nowrap;
}
body.admin .pagination a, body.admin .pagination span { border-radius: 10px; box-shadow: var(--el-1); }
/* A long unbreakable string must not push the page sideways.

   THE DEFECT (measured live 2026-09-18, 11 apps): the Settings colour-palette
   picker renders each option's description inside `.cat-tile`. Several of the
   seeded descriptions end in a parenthetical disambiguation list —
   "(steel-blue clinical grey, distinct from
   teal/mint/enamel/sage/sprout/steel/slate/periwinkle)" — and that slash-joined
   run is ONE ~50-character word with no break opportunity in it. The text
   wrapped happily for four lines and then the last line ran 281px out of a
   152px tile, taking the whole document to 483px in a 375px viewport.

   `break-word` and not `anywhere`: it breaks a word only when the word cannot
   fit on a line of its own, so ordinary prose still wraps between words.

   ⚠️ Deliberately NOT applied to `.stat .value`, which sets
   `overflow-wrap:normal` a few sections up for the opposite reason — a figure
   must never break mid-number. Those two rules look contradictory and are not:
   prose that cannot fit should break, a number that cannot fit should shrink.
*/
body.admin .cat-tile, body.admin .cat-tile span,
body.public .cat-tile, body.public .cat-tile span {
    overflow-wrap: break-word;
    min-width: 0;
}

body.admin .empty { padding: 38px 20px; }
body.admin .empty i { font-size: 40px; }
body.admin .msg, body.public .msg { border-radius: 12px; box-shadow: var(--el-1); }
body.admin .avatar { box-shadow: var(--el-1); }

/* Toolbar: the search field is the point, so it gets the room on a phone. */
@media (max-width: 680px) {
    body.admin .toolbar { gap: 8px; }
    body.admin .toolbar .search { flex: 1 1 100%; }
}

/* ============================================================================
   9 · PUBLIC / CUSTOMER SHELL
   ============================================================================ */
body.public .tabbar {
    background: rgba(255,255,255,.88);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
}
body.public .tabbar a { font-size: 10.5px; gap: 2px; }
body.public .tabbar a i { font-size: 21px; }
body.public .tabbar a:active i { transform: scale(.88); transition: transform var(--t-fast) var(--e-out); }
body.public .public-header {
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
}

/* ============================================================================
   10 · OPT-IN HELPERS (`.ap-*`). Nothing above depends on these; a view adopts
   one when it is next touched. Namespaced so they can never collide with the
   112 app-specific blocks appended to the various app.css copies.
   ============================================================================ */

/* A filter-chip strip that scrolls instead of wrapping to three lines.
   Put `.ap-chips` on the existing `.row.wrap` that holds the filter buttons.
   Measured on billora at 375px: seven filter buttons wrapped to 3 lines and ate
   129px — a sixth of the screen — before a single record was visible.

   ⚠️ ALL THREE SELECTORS ARE REQUIRED. The strip is almost always an existing
   `<div class="row wrap">`, and `.row.wrap` is (0,0,2,0) — it beats a lone
   `.ap-chips` at (0,0,1,0), so `flex-wrap:wrap` wins and the class appears to
   do nothing at all. It silently failed exactly that way the first time.
   Listing the real-world shapes is clearer than an !important. */
.ap-chips,
.row.ap-chips,
.row.wrap.ap-chips {
    display: flex; flex-wrap: nowrap; gap: 7px;
    overflow-x: auto; scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin: 0 -14px 14px; padding: 2px 14px 4px;
}
.ap-chips::-webkit-scrollbar { display: none; }
.ap-chips > * { flex: none; scroll-snap-align: start; }
@media (min-width: 1024px) {
    .ap-chips, .row.ap-chips, .row.wrap.ap-chips {
        flex-wrap: wrap; overflow: visible; margin-inline: 0; padding-inline: 0;
    }
}

/* Tinted icon chip for metric cards and list rows. */
.ap-ic {
    width: 32px; height: 32px; border-radius: 10px; flex: none;
    display: grid; place-items: center; font-size: 17px;
    background: var(--tb-slate); color: var(--t-slate);
}
.t-indigo  { background: var(--tb-indigo);  color: var(--t-indigo); }
.t-violet  { background: var(--tb-violet);  color: var(--t-violet); }
.t-rose    { background: var(--tb-rose);    color: var(--t-rose); }
.t-orange  { background: var(--tb-orange);  color: var(--t-orange); }
.t-amber   { background: var(--tb-amber);   color: var(--t-amber); }
.t-emerald { background: var(--tb-emerald); color: var(--t-emerald); }
.t-teal    { background: var(--tb-teal);    color: var(--t-teal); }
.t-sky     { background: var(--tb-sky);     color: var(--t-sky); }
.t-slate   { background: var(--tb-slate);   color: var(--t-slate); }

/* Trend chip. `flat` exists so an unchanged metric can say so rather than
   showing a misleading green arrow or nothing at all. */
.ap-delta {
    display: inline-flex; align-items: center; gap: 3px;
    padding: 2px 7px; border-radius: 999px;
    font-size: 11px; font-weight: 700; letter-spacing: 0;
    background: var(--tb-emerald); color: var(--t-emerald);
}
.ap-delta.down { background: var(--tb-rose); color: var(--t-rose); }
.ap-delta.flat { background: var(--gray-soft); color: var(--gray-badge); }
.ap-delta i { font-size: 12px; }

/* A section heading with an optional right-hand action. */
.ap-sec { display: flex; align-items: baseline; gap: 10px; margin: 22px 2px 10px; }
.ap-sec:first-child { margin-top: 2px; }
.ap-sec h2 { font-size: 15.5px; font-weight: 800; letter-spacing: -.02em; color: var(--ink); }
.ap-sec p { font-size: 12.5px; color: var(--muted); margin-top: 1px; }
.ap-sec .ap-sec-act { margin-left: auto; font-size: 12.5px; font-weight: 700; color: var(--blue); white-space: nowrap; }

/* ============================================================================
   11 · MOTION PREFERENCES — last, so it wins over everything above.
   ============================================================================ */
@media (prefers-reduced-motion: reduce) {
    body.admin *, body.public * {
        animation-duration: .001ms !important;
        transition-duration: .001ms !important;
    }
    body.admin .btn:active, body.admin .icon-btn:active,
    body.admin a.card:active, body.admin a.stat:active,
    body.public .btn:active { transform: none; }
}

/* ============================================================================
   12 · PRINTABLE DOCUMENTS — the one DELIBERATELY UNSCOPED rule in this file.

   Everything else here is scoped to `body.admin` / `body.public`. The printable
   documents — statement, invoice, receipt — render through the `bare` layout,
   whose <body> carries NO class at all, so every rule above is inert on them.
   That is why they were the last customer-facing defect left: appui.css is
   loaded on those pages and simply never matched anything.

   THE DEFECT: /portal/statement is a raw 6-column <table> with no `.table-wrap`
   around it. Measured live as the CUSTOMER on 2026-09-18: a 406px table in a
   375px viewport, 433px of document — on six apps (the whole billing family).
   A shop's own customer opens their account statement on a phone and it runs
   off the side.

   ⚠️ `@media screen` IS LOAD-BEARING. These pages auto-print; a bare
   `max-width` query would follow the document onto paper and turn a printed
   statement into a clipped, scrollable box. Print keeps the real table.

   `.doc` is defined in the canonical block, so this is still an override of
   something app.css owns — the only difference is that it cannot be scoped by
   body class, because on this surface there is no body class to scope to.
   ============================================================================ */
@media screen and (max-width: 720px) {
    .doc table {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        max-width: 100%;
    }
}

@media print {
    body.admin .card, body.admin .stat, body.admin .table-wrap { box-shadow: none !important; }
    body.admin .table th { position: static; }
}
