/* =========================================================================
   Quickser parent portal — ask.quickser.app
   Story 18.4. Hand-authored, mobile-first, no build step and no framework.

   ⚠️ THIS FILE EXISTS BECAUSE quickser-ui.css CANNOT BE REUSED HERE, NOT BECAUSE
   NOBODY LOOKED. 279 of that file's rules are prefixed `.voyager` — they are
   scoped to the body class the admin shell sets, so a parent layout that omits
   the class gets almost nothing, and one that adds it drags Voyager's 476 KB
   app.css onto a phone. The admin shell also pulls a DB-driven sidebar and a
   frozen minified bundle this repo cannot rebuild.

   🔴 THE TOKEN BLOCK BELOW IS A COPY OF quickser-ui.css's `:root` BLOCK (section "1. Tokens"),
   NOT A LINK. Cited by SYMBOL, not by line number, because project-context.md carries a standing
   rule about exactly this and 18.4's code review caught the first draft breaking it: that draft
   said "29-81", and the very same commit inserted a 7-line warning above `:root` and moved the
   block to 36-88. A citation that goes stale in the commit that writes it is the whole argument.
   A brand change touches BOTH files. The same warning is in that file. Sharing
   them would need the asset pipeline this repo deliberately does not have —
   the CSS is hand-authored and committed, and q_asset() cache-busts on
   filemtime rather than on a build hash.

   ⚠️ LIGHT ONLY, DELIBERATELY. The Option C mockup
   (.claude-scratch/ui-elements/parent-portal/option-c-insight.html) carries a
   prefers-color-scheme dark block; it is not carried here. Light is the
   product's default scheme, the admin's dark theme is a per-user data-theme
   toggle with a pre-paint script and no UI on a two-page portal, and a second
   palette is a second thing to keep accessible. `color-scheme: light` is set
   so the browser paints its OWN widgets — checkbox, autofill menu — to match
   rather than force-darkening them.
   ⚠️ THE COPY IS THE UPSTREAM SUBSET THE PORTAL NEEDS, NOT THE SUBSET THIS FILE USES TODAY.
   ✅ UPDATED BY STORY 18.5, WHICH SPENT MOST OF THE SLACK: --q-ochre, --q-emerald, --q-mono,
   --q-indigo-wash, --q-indigo-line and --q-shadow were declared and unreferenced until section 8
   below arrived, and all six are now in use. --q-rose-wash / --q-rose-line remain sign-in-only.
   The distinction is restated rather than deleted because the reciprocal comment in
   quickser-ui.css used to claim this was "the subset it uses", and that comment is what a brand
   change will be audited against — a token list that drifts from the truth is the failure mode.
   ========================================================================= */

/* ── 1. Tokens (copied — see the warning above) ──────────────────────────── */
:root {
    --q-indigo:        #2F4BA8;   /* brand indigo — 7.80:1 on white            */
    --q-indigo-soft:   #4761BE;
    --q-indigo-wash:   #eef1fb;
    --q-indigo-line:   #c9d3f2;
    --q-ochre:         #96590B;   /* brand ochre — 5.14:1 on canvas            */

    --q-canvas:        #f1f5f9;   /* app background                            */
    --q-surface:       #ffffff;   /* panels/cards                              */

    --q-ink:           #0f172a;   /* headings / primary text                   */
    --q-ink-2:         #334155;
    --q-ink-3:         #475569;
    --q-dim:           #64748b;   /* timestamps / tertiary                     */

    --q-line:          #e2e8f0;
    --q-line-strong:   #cbd5e1;

    --q-rose:          #b91c1c;   /* errors / negative money                   */
    --q-rose-wash:     #fef2f2;
    --q-rose-line:     #fecaca;
    --q-emerald:       #047857;

    --q-radius:        10px;
    --q-shadow:        0 1px 2px rgba(15,23,42,.06), 0 1px 3px rgba(15,23,42,.04);
    --q-shadow-lift:   0 4px 12px rgba(15,23,42,.10), 0 2px 4px rgba(15,23,42,.06);
    --q-mono:          "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, monospace;
    --q-nav:           "Plus Jakarta Sans", Inter, "Open Sans", sans-serif;

    /* Portal-local. Everything above is brand; everything here is this surface.
       --q-radius-lg is the phone-scale card radius Option C uses (16px) — larger
       than the admin's 10px because a parent sees one card, not a dense table. */
    --q-radius-lg:     16px;
    --q-focus:         0 0 0 3px rgba(47, 75, 168, .20);

    color-scheme: light;
}

/* ── 2. Reset and page ───────────────────────────────────────────────────── */
* { box-sizing: border-box; }

body {
    margin: 0;
    min-height: 100vh;
    /* Inter is self-hosted in public/fonts/fonts.css and is the face of the whole
       product; the system stack after it is the no-webfont fallback, not a design. */
    font: 15px/1.5 Inter, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    color: var(--q-ink);
    background: var(--q-canvas);
    -webkit-font-smoothing: antialiased;
}

/* ── 3. Brand lockup ─────────────────────────────────────────────────────── */
.lockup {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--q-nav);
    font-weight: 700;
    font-size: 19px;
    letter-spacing: -.02em;
    color: var(--q-ink);
}

/* The mark is public/images/quickser-logo.png — the FULL-COLOUR one. Do not swap it
   for quickser-mono.png hoping for a tidier lockup: that asset is solid black, drawn
   to be laid on a light plate, and it disappears the moment anyone puts a brand
   colour behind it. The source is 373x410, hence object-fit rather than a square. */
.lockup img {
    width: 28px;
    height: 30px;
    object-fit: contain;
}

/* ── 4. The sign-in screen ───────────────────────────────────────────────── */
/* Mobile-first: the card is the page. `min-height: 100svh` would be truer on a
   phone with a retracting URL bar, but 100vh is the value every target browser
   agrees on and the card is short enough that the difference is invisible. */
.auth {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 28px 18px 44px;
}

.auth-card {
    width: 100%;
    max-width: 400px;
    background: var(--q-surface);
    border: 1px solid var(--q-line);
    border-radius: var(--q-radius-lg);
    box-shadow: var(--q-shadow-lift);
    padding: 30px 26px 26px;
}

.auth-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    margin-bottom: 24px;
    text-align: center;
}

.auth-head .sub {
    font-size: 13.5px;
    color: var(--q-dim);
}

.auth-note {
    max-width: 400px;
    margin: 0;
    text-align: center;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--q-dim);
}

/* ── 5. Form controls ────────────────────────────────────────────────────── */
.field { margin-bottom: 16px; }

.field label {
    display: block;
    margin: 0 0 6px 2px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--q-ink-3);
}

/* `text` MUST stay in this selector list. The identifier input is deliberately
   type="text" and not type="email" — see the comment on the field itself — so an
   email-only selector would leave the one field a parent types into unstyled. */
input[type="text"],
input[type="password"] {
    width: 100%;
    padding: 11px 13px;
    font-size: 15px;   /* ⚠️ NOT SMALLER. iOS Safari zooms the viewport on focus
                          for any input under 16px; 15px is the practical floor
                          people report as safe, and this is a phone-first page. */
    font-family: inherit;
    color: var(--q-ink);
    background: var(--q-surface);
    border: 1px solid var(--q-line-strong);
    border-radius: var(--q-radius);
    outline: none;
    transition: border-color .12s ease, box-shadow .12s ease;
}

input::placeholder { color: var(--q-dim); }

/* 🔴 `outline`, NOT `box-shadow` ALONE — .btn:focus-visible in this file already got this right and
   these two did not. `box-shadow` is NOT PAINTED under Windows High Contrast / `forced-colors: active`,
   and the forced palette also replaces the border colour, so a keyboard user in that mode had NO
   visible focus on either field of a page that consists of two fields, a checkbox and a button.
   The soft ring is also not load-bearing on its own: --q-focus is rgba(47,75,168,.20) ≈ #d5daee over
   white, about 1.3:1, well under the WCAG 2.2 3:1 focus-appearance floor. The outline carries the
   requirement; the shadow and the border swap are the house style on top of it. */
input[type="text"]:focus,
input[type="password"]:focus {
    border-color: var(--q-indigo);
    box-shadow: var(--q-focus);
    outline: 2px solid var(--q-indigo);
    outline-offset: 1px;
}

/* WebKit paints its own opaque autofill background and ignores `background`; an
   inset shadow large enough to fill the field is the one reliable cover. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--q-ink);
    -webkit-box-shadow: 0 0 0 1000px var(--q-surface) inset;
    caret-color: var(--q-ink);
}

/* -webkit-box-shadow is a cascade ALIAS of box-shadow, so the cover above would
   silently replace the focus ring on an autofilled field — the most common state
   on a login page. Restating both keeps the ring; without it the only focus
   indicator left is the 1px border, because `outline` is none. */
input:-webkit-autofill:focus {
    -webkit-box-shadow: var(--q-focus), 0 0 0 1000px var(--q-surface) inset;
}

/* The alert takes focus when the page renders with errors (see login.blade.php), so it needs a
   visible indicator of its own — it is a tabindex="-1" container, not a control. */
.alert:focus-visible {
    outline: 2px solid var(--q-rose);
    outline-offset: 2px;
}

.check {
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 2px 2px 22px;
}

.check input {
    width: 17px;
    height: 17px;
    margin: 0;
    flex: none;
    accent-color: var(--q-indigo);
}

.check label {
    margin: 0;
    font-size: 13.5px;
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    color: var(--q-ink-2);
}

.btn {
    display: block;
    width: 100%;
    padding: 12px;
    font: inherit;
    font-size: 15px;
    font-weight: 600;
    color: #fff;
    background: var(--q-indigo);
    border: 0;
    border-radius: var(--q-radius);
    cursor: pointer;
    transition: background .12s ease;
}

.btn:hover { background: var(--q-indigo-soft); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--q-ink); outline-offset: 2px; }

/* ── 6. Error block ──────────────────────────────────────────────────────── */
/* One `auth.failed` message covers every sign-in failure by design — unknown
   email, wrong password, wrong role, deactivated account. Do not add per-case
   styling here hoping to tell them apart; the controller does not tell them
   apart, and ParentLoginTest asserts the four bags are byte-identical. */
.alert {
    margin-bottom: 20px;
    padding: 11px 13px;
    font-size: 13.5px;
    color: var(--q-rose);
    background: var(--q-rose-wash);
    border: 1px solid var(--q-rose-line);
    border-radius: var(--q-radius);
}

/* No list marker. The overwhelmingly common case is ONE message — `auth.failed` covers
   every sign-in failure by design — and a lone bullet reads as a fragment of a list that
   is not there. The <ul> stays because the multi-error case is real (both fields empty
   fails `required` twice) and it is what the $errors->all() loop produces. */
.alert ul { margin: 0; padding: 0; list-style: none; }
.alert li + li { margin-top: 4px; }

/* ── 7. Motion ───────────────────────────────────────────────────────────── */
/* The attribute selectors are load-bearing: a bare `input` here is (0,0,1) and
   LOSES the cascade to the (0,1,1) base rule — a media query adds no
   specificity — so it would never apply. */
@media (prefers-reduced-motion: reduce) {
    input[type="text"],
    input[type="password"],
    .btn { transition: none; }
    .btn:active { transform: none; }
}

/* ── 8. The signed-in surface (Story 18.5) ───────────────────────────────── */
/* Everything below this line is the authenticated chrome — the dashboard and, from
   18.6, the history page. Sections 1-7 above are the sign-in screen and the tokens.

   All six tokens the header block calls out — --q-ochre, --q-emerald, --q-mono,
   --q-indigo-wash, --q-indigo-line, --q-shadow — are used from here down. The
   duplication warning up there stays, because the point it makes (a brand change
   touches BOTH files) did not stop being true. */

.app { padding-bottom: 40px; }

/* ── 8.1 Top bar ─────────────────────────────────────────────────────────── */
.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    background: var(--q-surface);
    border-bottom: 1px solid var(--q-line);
}

.topbar .lockup { text-decoration: none; }

/* A button that reads as a link. It must stay a <button> inside a POST form — sign-out
   is a state change, and a GET link lets any page on the internet fire it with an <img>. */
.linkbtn {
    padding: 6px 2px;
    font: inherit;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--q-indigo);
    background: none;
    border: 0;
    cursor: pointer;
}

.linkbtn:hover { color: var(--q-indigo-soft); text-decoration: underline; }
.linkbtn:focus-visible { outline: 2px solid var(--q-indigo); outline-offset: 2px; border-radius: 4px; }

/* ── 8.2 Page frame ──────────────────────────────────────────────────────── */
/* Mobile-first single column; the max-width only bites on a desktop browser, where a
   full-bleed 1600px balance card would be absurd. */
.wrap {
    max-width: 520px;
    margin: 0 auto;
    padding: 16px 16px 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.panel {
    background: var(--q-surface);
    border: 1px solid var(--q-line);
    border-radius: var(--q-radius-lg);
    box-shadow: var(--q-shadow);
    padding: 18px 18px 16px;
}

.panel h2 {
    margin: 0 0 14px;
    font-family: var(--q-nav);
    font-size: 14px;
    font-weight: 700;
    letter-spacing: -.01em;
    color: var(--q-ink-2);
}

.panel-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.panel-head h2 { margin-bottom: 14px; }

.seeall {
    font-size: 13px;
    font-weight: 600;
    color: var(--q-indigo);
    text-decoration: none;
    white-space: nowrap;
}

.seeall:hover { text-decoration: underline; }

.note {
    margin: 12px 0 0;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--q-dim);
}

/* The unlinked-account panel. Four different causes render this one block; nothing here
   may grow a variant, because the page cannot tell those causes apart — see the view. */
.empty { text-align: center; padding: 30px 22px; }
.empty h1 {
    margin: 0 0 10px;
    font-family: var(--q-nav);
    font-size: 18px;
    font-weight: 700;
    color: var(--q-ink);
}
.empty p {
    margin: 0 0 10px;
    font-size: 13.5px;
    line-height: 1.65;
    color: var(--q-ink-3);
}
.empty p:last-child { margin-bottom: 0; }

/* ── 8.3 Balance card ────────────────────────────────────────────────────── */
/* The one saturated surface on the page. Everything else is white on canvas, so the
   number a parent came for is the only thing carrying the brand colour. */
.balance {
    padding: 22px 20px;
    border-radius: var(--q-radius-lg);
    background: linear-gradient(135deg, var(--q-indigo) 0%, var(--q-indigo-soft) 100%);
    box-shadow: var(--q-shadow-lift);
    color: #fff;
}

.balance-who {
    font-family: var(--q-nav);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -.01em;
}

.balance-label {
    margin-top: 14px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    /* Not --q-indigo-line: this sits on the gradient, not on canvas, so the muted tone
       has to be a translucency of white or it drifts as the gradient changes. */
    color: rgba(255, 255, 255, .78);
}

.balance-value {
    margin-top: 2px;
    font-family: var(--q-mono);
    font-size: 34px;
    font-weight: 600;
    letter-spacing: -.02em;
    line-height: 1.15;
}

.balance-free {
    margin-top: 6px;
    font-size: 12.5px;
    color: rgba(255, 255, 255, .8);
}

/* ── 8.4 Spend tiles ─────────────────────────────────────────────────────── */
.tiles {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.tile {
    background: var(--q-surface);
    border: 1px solid var(--q-line);
    border-radius: var(--q-radius-lg);
    box-shadow: var(--q-shadow);
    padding: 14px 16px;
}

.tile-label {
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--q-dim);
}

.tile-value {
    margin-top: 5px;
    font-family: var(--q-mono);
    font-size: 21px;
    font-weight: 600;
    letter-spacing: -.01em;
    color: var(--q-ink);
}

/* Fees and refunds. Ochre rather than rose: they are not errors and not spending —
   they are the two lines that explain a balance the purchases alone do not account for. */
.subtle { background: var(--q-indigo-wash); border-color: var(--q-indigo-line); }
.subtle .tile-value { font-size: 18px; color: var(--q-ochre); }

/* These two tiles are month-scoped and the other two name their window in the label,
   so this says which period they cover. Without it a refund that drops off on the 1st
   takes its explanation with it — see the view's comment for the case. */
.tile-period {
    margin-top: 2px;
    font-size: 11px;
    color: var(--q-dim);
}

/* ── 8.5 The 14-day trend ────────────────────────────────────────────────── */
/* 🔴 FOURTEEN <div>s AND NO CHART LIBRARY. public/js/echarts.min.js is a committed ~1 MB
   bundle whose entry file registers only Bar/Line/Pie + Grid/Tooltip/Legend and fails
   SILENTLY on anything else; this page loads no JavaScript at all and does not start now. */
.trend {
    display: flex;
    align-items: flex-end;
    gap: 4px;
    height: 96px;
}

.trend-col {
    flex: 1;
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* 🔴 THE TRACK EXISTS SO THE BARS ARE TO SCALE, AND ITS ABSENCE WAS A REAL DEFECT.
   A percentage height resolves against the PARENT's height. The bars used to hang
   directly off .trend-col — 96px, INCLUDING the 16px tick strip below them — while
   the painted rail was calc(100% - 16px). So a 100% bar asked for 96px in a box that
   also had to fit the tick: the tall end of the chart, the end a parent actually
   reads, either overflowed the panel or flex-shrank non-linearly, drawing ~0.58
   where the data said 0.50. The track is the rail; the bar is a percentage of it.

   The rail itself is why a quiet day is still a visible, empty column rather than a
   gap — which is the whole reason the series is zero-filled server-side. The bar on
   such a day is 0% (see the view: the height is floored to 2% only when the day has
   actual spending), so the rail is all that shows. */
.trend-track {
    flex: 1;
    display: flex;
    align-items: flex-end;
    min-height: 0;
    background: var(--q-indigo-wash);
    border-radius: 4px;
}

.trend-bar {
    width: 100%;
    min-height: 0;
    background: var(--q-indigo);
    border-radius: 4px 4px 0 0;
}

.trend-tick {
    height: 16px;
    padding-top: 4px;
    font-family: var(--q-mono);
    font-size: 9.5px;
    line-height: 1;
    text-align: center;
    color: var(--q-dim);
}

/* ── 8.6 Recent activity ─────────────────────────────────────────────────── */
/* 🔴 THE SEPARATOR IS `.entry + .entry`, NOT `.entry` PLUS A `:first-of-type` RESET.
   The reset shipped as `.entry:first-of-type { border-top: 0 }` and matched NOTHING:
   `:first-of-type` means "first sibling of that ELEMENT type", and the first div in
   the panel is div.panel-head, not an .entry. So every entry kept its top border and
   a stray rule floated under the "Recent activity" heading. `:first-child` would fail
   for the same reason. An adjacent-sibling rule needs no reset and cannot drift. */
.entry {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 14px;
    padding: 11px 0;
}

.entry + .entry { border-top: 1px solid var(--q-line); }

.entry-label {
    font-size: 14px;
    font-weight: 600;
    color: var(--q-ink);
}

.entry-when {
    margin-top: 2px;
    font-size: 12px;
    color: var(--q-dim);
}

/* Line items exist for almost none of the ledger — `items` is unrecoverable from the ASK
   export, so only orders rung since the 2026-08-31 go-live carry any. The block simply
   does not render when there are none; there is no "no detail" placeholder to add. */
.entry-lines {
    margin-top: 5px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px 6px;
}

.entry-line {
    padding: 2px 7px;
    font-size: 11.5px;
    color: var(--q-ink-3);
    background: var(--q-canvas);
    border: 1px solid var(--q-line);
    border-radius: 999px;
}

.entry-amount {
    flex: none;
    font-family: var(--q-mono);
    font-size: 14.5px;
    font-weight: 600;
    white-space: nowrap;
}

/* Direction, not judgement: `in` is money arriving on the band (a top-up or a refund),
   `out` is money leaving it. Emerald and ink rather than emerald and rose — a purchase
   is the normal event on this page and must not read as an error. */
.entry-amount.in { color: var(--q-emerald); }
.entry-amount.out { color: var(--q-ink-2); }

/* ── 8.7 Wider screens ───────────────────────────────────────────────────── */
/* One breakpoint. The portal is a phone surface; this stops it looking broken on a
   laptop, and does not pretend to be a desktop layout. */
@media (min-width: 560px) {
    .wrap { padding-top: 22px; gap: 16px; }
    .balance-value { font-size: 38px; }
    .trend { height: 120px; }
}
